diff --git a/.version b/.version
index 22e3b6b01..ccc99d021 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-2.11.3
+2.12.3
diff --git a/Jenkinsfile b/Jenkinsfile
index 1d8680a6b..66ed7cb6a 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -43,7 +43,7 @@ pipeline {
 					steps {
 						script {
 							// Defaults to the Branch name, which is applies to all branches AND pr's
-							buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
+							buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
 						}
 					}
 				}
@@ -56,6 +56,13 @@ pipeline {
 						sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
 					}
 				}
+				stage('Docker Login') {
+					steps {
+						withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
+							sh 'docker login -u "${duser}" -p "${dpass}"'
+						}
+					}
+				}
 			}
 		}
 		stage('Builds') {
@@ -120,6 +127,11 @@ pipeline {
 					junit 'test/results/junit/*'
 					sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
 				}
+				unstable {
+					dir(path: 'test/results') {
+						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
+					}
+				}
 			}
 		}
 		stage('Test Mysql') {
@@ -148,20 +160,60 @@ pipeline {
 					junit 'test/results/junit/*'
 					sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
 				}
+				unstable {
+					dir(path: 'test/results') {
+						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
+					}
+				}
 			}
 		}
-		stage('MultiArch Build') {
+		stage('Test Postgres') {
+			environment {
+				COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
+				COMPOSE_FILE         = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
+			}
 			when {
 				not {
 					equals expected: 'UNSTABLE', actual: currentBuild.result
 				}
 			}
 			steps {
-				withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
-					sh 'docker login -u "${duser}" -p "${dpass}"'
-					sh "./scripts/buildx --push ${buildxPushTags}"
+				sh 'rm -rf ./test/results/junit/*'
+				sh './scripts/ci/fulltest-cypress'
+			}
+			post {
+				always {
+					// Dumps to analyze later
+					sh 'mkdir -p debug/postgres'
+					sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
+					sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
+
+					junit 'test/results/junit/*'
+					sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
+				}
+				unstable {
+					dir(path: 'test/results') {
+						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
+					}
+				}
+			}
+		}
+		stage('MultiArch Build') {
+			when {
+				not {
+					equals expected: 'UNSTABLE', actual: currentBuild.result
 				}
 			}
+			steps {
+				sh "./scripts/buildx --push ${buildxPushTags}"
+			}
 		}
 		stage('Docs / Comment') {
 			parallel {
@@ -189,7 +241,13 @@ pipeline {
 					}
 					steps {
 						script {
-							npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true)
+							npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on
+[DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev)
+as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}`
+
+**Note:** ensure you backup your NPM instance before testing this image! Especially if there are database changes
+**Note:** this is a different docker image namespace than the official image
+""", true)
 						}
 					}
 				}
@@ -200,20 +258,13 @@ pipeline {
 		always {
 			sh 'echo Reverting ownership'
 			sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data'
-		}
-		success {
-			juxtapose event: 'success'
-			sh 'figlet "SUCCESS"'
+			printResult(true)
 		}
 		failure {
 			archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
-			juxtapose event: 'failure'
-			sh 'figlet "FAILURE"'
 		}
 		unstable {
 			archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
-			juxtapose event: 'unstable'
-			sh 'figlet "UNSTABLE"'
 		}
 	}
 }
diff --git a/README.md b/README.md
index 55a986d18..925aeb23d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 <p align="center">
 	<img src="https://nginxproxymanager.com/github.png">
 	<br><br>
-	<img src="https://img.shields.io/badge/version-2.11.3-green.svg?style=for-the-badge">
+	<img src="https://img.shields.io/badge/version-2.12.3-green.svg?style=for-the-badge">
 	<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
 		<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
 	</a>
diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json
deleted file mode 100644
index 4e540ab30..000000000
--- a/backend/.vscode/settings.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-	"editor.insertSpaces": false,
-	"editor.formatOnSave": true,
-	"files.trimTrailingWhitespace": true,
-	"editor.codeActionsOnSave": {
-		"source.fixAll.eslint": true
-	}
-}
\ No newline at end of file
diff --git a/backend/app.js b/backend/app.js
index e528a0bbe..59f7def20 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -52,7 +52,7 @@ app.use(function (req, res, next) {
 });
 
 app.use(require('./lib/express/jwt')());
-app.use('/', require('./routes/api/main'));
+app.use('/', require('./routes/main'));
 
 // production error handler
 // no stacktraces leaked to user
diff --git a/backend/config/default.json b/backend/config/default.json
index 64ab577c8..154e66e48 100644
--- a/backend/config/default.json
+++ b/backend/config/default.json
@@ -1,6 +1,6 @@
 {
   "database": {
-    "engine": "mysql",
+    "engine": "mysql2",
     "host": "db",
     "name": "npm",
     "user": "npm",
diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json
deleted file mode 100644
index 3fa19fc4b..000000000
--- a/backend/doc/api.swagger.json
+++ /dev/null
@@ -1,1456 +0,0 @@
-{
-	"openapi": "3.0.0",
-	"info": {
-		"title": "Nginx Proxy Manager API",
-		"version": "2.x.x"
-	},
-	"servers": [
-		{
-			"url": "http://127.0.0.1:81/api"
-		}
-	],
-	"paths": {
-		"/": {
-			"get": {
-				"operationId": "health",
-				"summary": "Returns the API health status",
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"status": "OK",
-											"version": {
-												"major": 2,
-												"minor": 1,
-												"revision": 0
-											}
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/HealthObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/nginx/proxy-hosts": {
-			"get": {
-				"operationId": "getProxyHosts",
-				"summary": "Get all proxy hosts",
-				"tags": ["Proxy Hosts"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "query",
-						"name": "expand",
-						"description": "Expansions",
-						"schema": {
-							"type": "string",
-							"enum": ["access_list", "owner", "certificate"]
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": [
-											{
-												"id": 1,
-												"created_on": "2023-03-30T01:12:23.000Z",
-												"modified_on": "2023-03-30T02:15:40.000Z",
-												"owner_user_id": 1,
-												"domain_names": ["aasdasdad"],
-												"forward_host": "asdasd",
-												"forward_port": 80,
-												"access_list_id": 0,
-												"certificate_id": 0,
-												"ssl_forced": 0,
-												"caching_enabled": 0,
-												"block_exploits": 0,
-												"advanced_config": "sdfsdfsdf",
-												"meta": {
-													"letsencrypt_agree": false,
-													"dns_challenge": false,
-													"nginx_online": false,
-													"nginx_err": "Command failed: /usr/sbin/nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n"
-												},
-												"allow_websocket_upgrade": 0,
-												"http2_support": 0,
-												"forward_scheme": "http",
-												"enabled": 1,
-												"locations": [],
-												"hsts_enabled": 0,
-												"hsts_subdomains": 0,
-												"owner": {
-													"id": 1,
-													"created_on": "2023-03-30T01:11:50.000Z",
-													"modified_on": "2023-03-30T01:11:50.000Z",
-													"is_deleted": 0,
-													"is_disabled": 0,
-													"email": "admin@example.com",
-													"name": "Administrator",
-													"nickname": "Admin",
-													"avatar": "",
-													"roles": ["admin"]
-												},
-												"access_list": null,
-												"certificate": null
-											},
-											{
-												"id": 2,
-												"created_on": "2023-03-30T02:11:49.000Z",
-												"modified_on": "2023-03-30T02:11:49.000Z",
-												"owner_user_id": 1,
-												"domain_names": ["test.example.com"],
-												"forward_host": "1.1.1.1",
-												"forward_port": 80,
-												"access_list_id": 0,
-												"certificate_id": 0,
-												"ssl_forced": 0,
-												"caching_enabled": 0,
-												"block_exploits": 0,
-												"advanced_config": "",
-												"meta": {
-													"letsencrypt_agree": false,
-													"dns_challenge": false,
-													"nginx_online": true,
-													"nginx_err": null
-												},
-												"allow_websocket_upgrade": 0,
-												"http2_support": 0,
-												"forward_scheme": "http",
-												"enabled": 1,
-												"locations": [],
-												"hsts_enabled": 0,
-												"hsts_subdomains": 0,
-												"owner": {
-													"id": 1,
-													"created_on": "2023-03-30T01:11:50.000Z",
-													"modified_on": "2023-03-30T01:11:50.000Z",
-													"is_deleted": 0,
-													"is_disabled": 0,
-													"email": "admin@example.com",
-													"name": "Administrator",
-													"nickname": "Admin",
-													"avatar": "",
-													"roles": ["admin"]
-												},
-												"access_list": null,
-												"certificate": null
-											}
-										]
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/ProxyHostsList"
-								}
-							}
-						}
-					}
-				}
-			},
-			"post": {
-				"operationId": "createProxyHost",
-				"summary": "Create a Proxy Host",
-				"tags": ["Proxy Hosts"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "body",
-						"name": "proxyhost",
-						"description": "Proxy Host Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/ProxyHostObject"
-						}
-					}
-				],
-				"responses": {
-					"201": {
-						"description": "201 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": 3,
-											"created_on": "2023-03-30T02:31:27.000Z",
-											"modified_on": "2023-03-30T02:31:27.000Z",
-											"owner_user_id": 1,
-											"domain_names": ["test2.example.com"],
-											"forward_host": "1.1.1.1",
-											"forward_port": 80,
-											"access_list_id": 0,
-											"certificate_id": 0,
-											"ssl_forced": 0,
-											"caching_enabled": 0,
-											"block_exploits": 0,
-											"advanced_config": "",
-											"meta": {
-												"letsencrypt_agree": false,
-												"dns_challenge": false
-											},
-											"allow_websocket_upgrade": 0,
-											"http2_support": 0,
-											"forward_scheme": "http",
-											"enabled": 1,
-											"locations": [],
-											"hsts_enabled": 0,
-											"hsts_subdomains": 0,
-											"certificate": null,
-											"owner": {
-												"id": 1,
-												"created_on": "2023-03-30T01:11:50.000Z",
-												"modified_on": "2023-03-30T01:11:50.000Z",
-												"is_deleted": 0,
-												"is_disabled": 0,
-												"email": "admin@example.com",
-												"name": "Administrator",
-												"nickname": "Admin",
-												"avatar": "",
-												"roles": ["admin"]
-											},
-											"access_list": null,
-											"use_default_location": true,
-											"ipv6": true
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/ProxyHostObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/schema": {
-			"get": {
-				"operationId": "schema",
-				"responses": {
-					"200": {
-						"description": "200 response"
-					}
-				},
-				"summary": "Returns this swagger API schema"
-			}
-		},
-		"/tokens": {
-			"get": {
-				"operationId": "refreshToken",
-				"summary": "Refresh your access token",
-				"tags": ["Tokens"],
-				"security": [
-					{
-						"BearerAuth": ["tokens"]
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"expires": 1566540510,
-											"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/TokenObject"
-								}
-							}
-						}
-					}
-				}
-			},
-			"post": {
-				"operationId": "requestToken",
-				"parameters": [
-					{
-						"description": "Credentials Payload",
-						"in": "body",
-						"name": "credentials",
-						"required": true,
-						"schema": {
-							"additionalProperties": false,
-							"properties": {
-								"identity": {
-									"minLength": 1,
-									"type": "string"
-								},
-								"scope": {
-									"minLength": 1,
-									"type": "string",
-									"enum": ["user"]
-								},
-								"secret": {
-									"minLength": 1,
-									"type": "string"
-								}
-							},
-							"required": ["identity", "secret"],
-							"type": "object"
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"result": {
-												"expires": 1566540510,
-												"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
-											}
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/TokenObject"
-								}
-							}
-						},
-						"description": "200 response"
-					}
-				},
-				"summary": "Request a new access token from credentials",
-				"tags": ["Tokens"]
-			}
-		},
-		"/settings": {
-			"get": {
-				"operationId": "getSettings",
-				"summary": "Get all settings",
-				"tags": ["Settings"],
-				"security": [
-					{
-						"BearerAuth": ["settings"]
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": [
-											{
-												"id": "default-site",
-												"name": "Default Site",
-												"description": "What to show when Nginx is hit with an unknown Host",
-												"value": "congratulations",
-												"meta": {}
-											}
-										]
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/SettingsList"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/settings/{settingID}": {
-			"get": {
-				"operationId": "getSetting",
-				"summary": "Get a setting",
-				"tags": ["Settings"],
-				"security": [
-					{
-						"BearerAuth": ["settings"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "settingID",
-						"schema": {
-							"type": "string",
-							"minLength": 1
-						},
-						"required": true,
-						"description": "Setting ID",
-						"example": "default-site"
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": "default-site",
-											"name": "Default Site",
-											"description": "What to show when Nginx is hit with an unknown Host",
-											"value": "congratulations",
-											"meta": {}
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/SettingObject"
-								}
-							}
-						}
-					}
-				}
-			},
-			"put": {
-				"operationId": "updateSetting",
-				"summary": "Update a setting",
-				"tags": ["Settings"],
-				"security": [
-					{
-						"BearerAuth": ["settings"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "settingID",
-						"schema": {
-							"type": "string",
-							"minLength": 1
-						},
-						"required": true,
-						"description": "Setting ID",
-						"example": "default-site"
-					},
-					{
-						"in": "body",
-						"name": "setting",
-						"description": "Setting Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/SettingObject"
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": "default-site",
-											"name": "Default Site",
-											"description": "What to show when Nginx is hit with an unknown Host",
-											"value": "congratulations",
-											"meta": {}
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/SettingObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/users": {
-			"get": {
-				"operationId": "getUsers",
-				"summary": "Get all users",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "query",
-						"name": "expand",
-						"description": "Expansions",
-						"schema": {
-							"type": "string",
-							"enum": ["permissions"]
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": [
-											{
-												"id": 1,
-												"created_on": "2020-01-30T09:36:08.000Z",
-												"modified_on": "2020-01-30T09:41:04.000Z",
-												"is_disabled": 0,
-												"email": "jc@jc21.com",
-												"name": "Jamie Curnow",
-												"nickname": "James",
-												"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
-												"roles": ["admin"]
-											}
-										]
-									},
-									"withPermissions": {
-										"value": [
-											{
-												"id": 1,
-												"created_on": "2020-01-30T09:36:08.000Z",
-												"modified_on": "2020-01-30T09:41:04.000Z",
-												"is_disabled": 0,
-												"email": "jc@jc21.com",
-												"name": "Jamie Curnow",
-												"nickname": "James",
-												"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
-												"roles": ["admin"],
-												"permissions": {
-													"visibility": "all",
-													"proxy_hosts": "manage",
-													"redirection_hosts": "manage",
-													"dead_hosts": "manage",
-													"streams": "manage",
-													"access_lists": "manage",
-													"certificates": "manage"
-												}
-											}
-										]
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/UsersList"
-								}
-							}
-						}
-					}
-				}
-			},
-			"post": {
-				"operationId": "createUser",
-				"summary": "Create a User",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "body",
-						"name": "user",
-						"description": "User Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/UserObject"
-						}
-					}
-				],
-				"responses": {
-					"201": {
-						"description": "201 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": 2,
-											"created_on": "2020-01-30T09:36:08.000Z",
-											"modified_on": "2020-01-30T09:41:04.000Z",
-											"is_disabled": 0,
-											"email": "jc@jc21.com",
-											"name": "Jamie Curnow",
-											"nickname": "James",
-											"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
-											"roles": ["admin"],
-											"permissions": {
-												"visibility": "all",
-												"proxy_hosts": "manage",
-												"redirection_hosts": "manage",
-												"dead_hosts": "manage",
-												"streams": "manage",
-												"access_lists": "manage",
-												"certificates": "manage"
-											}
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/UserObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/users/{userID}": {
-			"get": {
-				"operationId": "getUser",
-				"summary": "Get a user",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"oneOf": [
-								{
-									"type": "string",
-									"pattern": "^me$"
-								},
-								{
-									"type": "integer",
-									"minimum": 1
-								}
-							]
-						},
-						"required": true,
-						"description": "User ID or 'me' for yourself",
-						"example": 1
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": 1,
-											"created_on": "2020-01-30T09:36:08.000Z",
-											"modified_on": "2020-01-30T09:41:04.000Z",
-											"is_disabled": 0,
-											"email": "jc@jc21.com",
-											"name": "Jamie Curnow",
-											"nickname": "James",
-											"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
-											"roles": ["admin"]
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/UserObject"
-								}
-							}
-						}
-					}
-				}
-			},
-			"put": {
-				"operationId": "updateUser",
-				"summary": "Update a User",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"oneOf": [
-								{
-									"type": "string",
-									"pattern": "^me$"
-								},
-								{
-									"type": "integer",
-									"minimum": 1
-								}
-							]
-						},
-						"required": true,
-						"description": "User ID or 'me' for yourself",
-						"example": 2
-					},
-					{
-						"in": "body",
-						"name": "user",
-						"description": "User Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/UserObject"
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"id": 2,
-											"created_on": "2020-01-30T09:36:08.000Z",
-											"modified_on": "2020-01-30T09:41:04.000Z",
-											"is_disabled": 0,
-											"email": "jc@jc21.com",
-											"name": "Jamie Curnow",
-											"nickname": "James",
-											"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
-											"roles": ["admin"]
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/UserObject"
-								}
-							}
-						}
-					}
-				}
-			},
-			"delete": {
-				"operationId": "deleteUser",
-				"summary": "Delete a User",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"type": "integer",
-							"minimum": 1
-						},
-						"required": true,
-						"description": "User ID",
-						"example": 2
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": true
-									}
-								},
-								"schema": {
-									"type": "boolean"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/users/{userID}/auth": {
-			"put": {
-				"operationId": "updateUserAuth",
-				"summary": "Update a User's Authentication",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"oneOf": [
-								{
-									"type": "string",
-									"pattern": "^me$"
-								},
-								{
-									"type": "integer",
-									"minimum": 1
-								}
-							]
-						},
-						"required": true,
-						"description": "User ID or 'me' for yourself",
-						"example": 2
-					},
-					{
-						"in": "body",
-						"name": "user",
-						"description": "User Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/AuthObject"
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": true
-									}
-								},
-								"schema": {
-									"type": "boolean"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/users/{userID}/permissions": {
-			"put": {
-				"operationId": "updateUserPermissions",
-				"summary": "Update a User's Permissions",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"type": "integer",
-							"minimum": 1
-						},
-						"required": true,
-						"description": "User ID",
-						"example": 2
-					},
-					{
-						"in": "body",
-						"name": "user",
-						"description": "Permissions Payload",
-						"required": true,
-						"schema": {
-							"$ref": "#/components/schemas/PermissionsObject"
-						}
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": true
-									}
-								},
-								"schema": {
-									"type": "boolean"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/users/{userID}/login": {
-			"put": {
-				"operationId": "loginAsUser",
-				"summary": "Login as this user",
-				"tags": ["Users"],
-				"security": [
-					{
-						"BearerAuth": ["users"]
-					}
-				],
-				"parameters": [
-					{
-						"in": "path",
-						"name": "userID",
-						"schema": {
-							"type": "integer",
-							"minimum": 1
-						},
-						"required": true,
-						"description": "User ID",
-						"example": 2
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg",
-											"expires": "2020-01-31T10:56:23.239Z",
-											"user": {
-												"id": 1,
-												"created_on": "2020-01-30T10:43:44.000Z",
-												"modified_on": "2020-01-30T10:43:44.000Z",
-												"is_disabled": 0,
-												"email": "jc@jc21.com",
-												"name": "Jamie Curnow",
-												"nickname": "James",
-												"avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm",
-												"roles": ["admin"]
-											}
-										}
-									}
-								},
-								"schema": {
-									"type": "object",
-									"description": "Login object",
-									"required": ["expires", "token", "user"],
-									"additionalProperties": false,
-									"properties": {
-										"expires": {
-											"description": "Token Expiry Unix Time",
-											"example": 1566540249,
-											"minimum": 1,
-											"type": "number"
-										},
-										"token": {
-											"description": "JWT Token",
-											"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
-											"type": "string"
-										},
-										"user": {
-											"$ref": "#/components/schemas/UserObject"
-										}
-									}
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/reports/hosts": {
-			"get": {
-				"operationId": "reportsHosts",
-				"summary": "Report on Host Statistics",
-				"tags": ["Reports"],
-				"security": [
-					{
-						"BearerAuth": ["reports"]
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"proxy": 20,
-											"redirection": 1,
-											"stream": 0,
-											"dead": 1
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/HostReportObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		},
-		"/audit-log": {
-			"get": {
-				"operationId": "getAuditLog",
-				"summary": "Get Audit Log",
-				"tags": ["Audit Log"],
-				"security": [
-					{
-						"BearerAuth": ["audit-log"]
-					}
-				],
-				"responses": {
-					"200": {
-						"description": "200 response",
-						"content": {
-							"application/json": {
-								"examples": {
-									"default": {
-										"value": {
-											"proxy": 20,
-											"redirection": 1,
-											"stream": 0,
-											"dead": 1
-										}
-									}
-								},
-								"schema": {
-									"$ref": "#/components/schemas/HostReportObject"
-								}
-							}
-						}
-					}
-				}
-			}
-		}
-	},
-	"components": {
-		"securitySchemes": {
-			"BearerAuth": {
-				"type": "http",
-				"scheme": "bearer"
-			}
-		},
-		"schemas": {
-			"HealthObject": {
-				"type": "object",
-				"description": "Health object",
-				"additionalProperties": false,
-				"required": ["status", "version"],
-				"properties": {
-					"status": {
-						"type": "string",
-						"description": "Healthy",
-						"example": "OK"
-					},
-					"version": {
-						"type": "object",
-						"description": "The version object",
-						"example": {
-							"major": 2,
-							"minor": 0,
-							"revision": 0
-						},
-						"additionalProperties": false,
-						"required": ["major", "minor", "revision"],
-						"properties": {
-							"major": {
-								"type": "integer",
-								"minimum": 0
-							},
-							"minor": {
-								"type": "integer",
-								"minimum": 0
-							},
-							"revision": {
-								"type": "integer",
-								"minimum": 0
-							}
-						}
-					}
-				}
-			},
-			"TokenObject": {
-				"type": "object",
-				"description": "Token object",
-				"required": ["expires", "token"],
-				"additionalProperties": false,
-				"properties": {
-					"expires": {
-						"description": "Token Expiry Unix Time",
-						"example": 1566540249,
-						"minimum": 1,
-						"type": "number"
-					},
-					"token": {
-						"description": "JWT Token",
-						"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
-						"type": "string"
-					}
-				}
-			},
-			"ProxyHostObject": {
-				"type": "object",
-				"description": "Proxy Host object",
-				"required": [
-					"id",
-					"created_on",
-					"modified_on",
-					"owner_user_id",
-					"domain_names",
-					"forward_host",
-					"forward_port",
-					"access_list_id",
-					"certificate_id",
-					"ssl_forced",
-					"caching_enabled",
-					"block_exploits",
-					"advanced_config",
-					"meta",
-					"allow_websocket_upgrade",
-					"http2_support",
-					"forward_scheme",
-					"enabled",
-					"locations",
-					"hsts_enabled",
-					"hsts_subdomains",
-					"certificate",
-					"use_default_location",
-					"ipv6"
-				],
-				"additionalProperties": false,
-				"properties": {
-					"id": {
-						"type": "integer",
-						"description": "Proxy Host ID",
-						"minimum": 1,
-						"example": 1
-					},
-					"created_on": {
-						"type": "string",
-						"description": "Created Date",
-						"example": "2020-01-30T09:36:08.000Z"
-					},
-					"modified_on": {
-						"type": "string",
-						"description": "Modified Date",
-						"example": "2020-01-30T09:41:04.000Z"
-					},
-					"owner_user_id": {
-						"type": "integer",
-						"minimum": 1,
-						"example": 1
-					},
-					"domain_names": {
-						"type": "array",
-						"minItems": 1,
-						"items": {
-							"type": "string",
-							"minLength": 1
-						}
-					},
-					"forward_host": {
-						"type": "string",
-						"minLength": 1
-					},
-					"forward_port": {
-						"type": "integer",
-						"minimum": 1
-					},
-					"access_list_id": {
-						"type": "integer"
-					},
-					"certificate_id": {
-						"type": "integer"
-					},
-					"ssl_forced": {
-						"type": "integer"
-					},
-					"caching_enabled": {
-						"type": "integer"
-					},
-					"block_exploits": {
-						"type": "integer"
-					},
-					"advanced_config": {
-						"type": "string"
-					},
-					"meta": {
-						"type": "object"
-					},
-					"allow_websocket_upgrade": {
-						"type": "integer"
-					},
-					"http2_support": {
-						"type": "integer"
-					},
-					"forward_scheme": {
-						"type": "string"
-					},
-					"enabled": {
-						"type": "integer"
-					},
-					"locations": {
-						"type": "array"
-					},
-					"hsts_enabled": {
-						"type": "integer"
-					},
-					"hsts_subdomains": {
-						"type": "integer"
-					},
-					"certificate": {
-						"type": "object",
-						"nullable": true
-					},
-					"owner": {
-						"type": "object",
-						"nullable": true
-					},
-					"access_list": {
-						"type": "object",
-						"nullable": true
-					},
-					"use_default_location": {
-						"type": "boolean"
-					},
-					"ipv6": {
-						"type": "boolean"
-					}
-				}
-			},
-			"ProxyHostsList": {
-				"type": "array",
-				"description": "Proxyn Hosts list",
-				"items": {
-					"$ref": "#/components/schemas/ProxyHostObject"
-				}
-			},
-			"SettingObject": {
-				"type": "object",
-				"description": "Setting object",
-				"required": ["id", "name", "description", "value", "meta"],
-				"additionalProperties": false,
-				"properties": {
-					"id": {
-						"type": "string",
-						"description": "Setting ID",
-						"minLength": 1,
-						"example": "default-site"
-					},
-					"name": {
-						"type": "string",
-						"description": "Setting Display Name",
-						"minLength": 1,
-						"example": "Default Site"
-					},
-					"description": {
-						"type": "string",
-						"description": "Meaningful description",
-						"minLength": 1,
-						"example": "What to show when Nginx is hit with an unknown Host"
-					},
-					"value": {
-						"description": "Value in almost any form",
-						"example": "congratulations",
-						"oneOf": [
-							{
-								"type": "string",
-								"minLength": 1
-							},
-							{
-								"type": "integer"
-							},
-							{
-								"type": "object"
-							},
-							{
-								"type": "number"
-							},
-							{
-								"type": "array"
-							}
-						]
-					},
-					"meta": {
-						"description": "Extra metadata",
-						"example": {},
-						"type": "object"
-					}
-				}
-			},
-			"SettingsList": {
-				"type": "array",
-				"description": "Setting list",
-				"items": {
-					"$ref": "#/components/schemas/SettingObject"
-				}
-			},
-			"UserObject": {
-				"type": "object",
-				"description": "User object",
-				"required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"],
-				"additionalProperties": false,
-				"properties": {
-					"id": {
-						"type": "integer",
-						"description": "User ID",
-						"minimum": 1,
-						"example": 1
-					},
-					"created_on": {
-						"type": "string",
-						"description": "Created Date",
-						"example": "2020-01-30T09:36:08.000Z"
-					},
-					"modified_on": {
-						"type": "string",
-						"description": "Modified Date",
-						"example": "2020-01-30T09:41:04.000Z"
-					},
-					"is_disabled": {
-						"type": "integer",
-						"minimum": 0,
-						"maximum": 1,
-						"description": "Is user Disabled (0 = false, 1 = true)",
-						"example": 0
-					},
-					"email": {
-						"type": "string",
-						"description": "Email",
-						"minLength": 3,
-						"example": "jc@jc21.com"
-					},
-					"name": {
-						"type": "string",
-						"description": "Name",
-						"minLength": 1,
-						"example": "Jamie Curnow"
-					},
-					"nickname": {
-						"type": "string",
-						"description": "Nickname",
-						"example": "James"
-					},
-					"avatar": {
-						"type": "string",
-						"description": "Gravatar URL based on email, without scheme",
-						"example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm"
-					},
-					"roles": {
-						"description": "Roles applied",
-						"example": ["admin"],
-						"type": "array",
-						"items": {
-							"type": "string"
-						}
-					}
-				}
-			},
-			"UsersList": {
-				"type": "array",
-				"description": "User list",
-				"items": {
-					"$ref": "#/components/schemas/UserObject"
-				}
-			},
-			"AuthObject": {
-				"type": "object",
-				"description": "Authentication Object",
-				"required": ["type", "secret"],
-				"properties": {
-					"type": {
-						"type": "string",
-						"pattern": "^password$",
-						"example": "password"
-					},
-					"current": {
-						"type": "string",
-						"minLength": 1,
-						"maxLength": 64,
-						"example": "changeme"
-					},
-					"secret": {
-						"type": "string",
-						"minLength": 8,
-						"maxLength": 64,
-						"example": "mySuperN3wP@ssword!"
-					}
-				}
-			},
-			"PermissionsObject": {
-				"type": "object",
-				"properties": {
-					"visibility": {
-						"type": "string",
-						"description": "Visibility Type",
-						"enum": ["all", "user"]
-					},
-					"access_lists": {
-						"type": "string",
-						"description": "Access Lists Permissions",
-						"enum": ["hidden", "view", "manage"]
-					},
-					"dead_hosts": {
-						"type": "string",
-						"description": "404 Hosts Permissions",
-						"enum": ["hidden", "view", "manage"]
-					},
-					"proxy_hosts": {
-						"type": "string",
-						"description": "Proxy Hosts Permissions",
-						"enum": ["hidden", "view", "manage"]
-					},
-					"redirection_hosts": {
-						"type": "string",
-						"description": "Redirection Permissions",
-						"enum": ["hidden", "view", "manage"]
-					},
-					"streams": {
-						"type": "string",
-						"description": "Streams Permissions",
-						"enum": ["hidden", "view", "manage"]
-					},
-					"certificates": {
-						"type": "string",
-						"description": "Certificates Permissions",
-						"enum": ["hidden", "view", "manage"]
-					}
-				}
-			},
-			"HostReportObject": {
-				"type": "object",
-				"properties": {
-					"proxy": {
-						"type": "integer",
-						"description": "Proxy Hosts Count"
-					},
-					"redirection": {
-						"type": "integer",
-						"description": "Redirection Hosts Count"
-					},
-					"stream": {
-						"type": "integer",
-						"description": "Streams Count"
-					},
-					"dead": {
-						"type": "integer",
-						"description": "404 Hosts Count"
-					}
-				}
-			}
-		}
-	}
-}
diff --git a/backend/index.js b/backend/index.js
index 3d6d60071..551378251 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -1,23 +1,20 @@
 #!/usr/bin/env node
 
+const schema = require('./schema');
 const logger = require('./logger').global;
 
 async function appStart () {
 	const migrate             = require('./migrate');
 	const setup               = require('./setup');
 	const app                 = require('./app');
-	const apiValidator        = require('./lib/validator/api');
 	const internalCertificate = require('./internal/certificate');
 	const internalIpRanges    = require('./internal/ip_ranges');
 
 	return migrate.latest()
 		.then(setup)
-		.then(() => {
-			return apiValidator.loadSchemas;
-		})
+		.then(schema.getCompiledSchema)
 		.then(internalIpRanges.fetch)
 		.then(() => {
-
 			internalCertificate.initTimer();
 			internalIpRanges.initTimer();
 
@@ -34,7 +31,7 @@ async function appStart () {
 			});
 		})
 		.catch((err) => {
-			logger.error(err.message);
+			logger.error(err.message, err);
 			setTimeout(appStart, 1000);
 		});
 }
diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js
index 017fc738c..f6043e18b 100644
--- a/backend/internal/access-list.js
+++ b/backend/internal/access-list.js
@@ -81,7 +81,7 @@ const internalAccessList = {
 
 				return internalAccessList.build(row)
 					.then(() => {
-						if (row.proxy_host_count) {
+						if (parseInt(row.proxy_host_count, 10)) {
 							return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
 						}
 					})
@@ -223,7 +223,7 @@ const internalAccessList = {
 			.then((row) => {
 				return internalAccessList.build(row)
 					.then(() => {
-						if (row.proxy_host_count) {
+						if (parseInt(row.proxy_host_count, 10)) {
 							return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
 						}
 					}).then(internalNginx.reload)
@@ -252,9 +252,13 @@ const internalAccessList = {
 				let query = accessListModel
 					.query()
 					.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
-					.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
+					.leftJoin('proxy_host', function() {
+						this.on('proxy_host.access_list_id', '=', 'access_list.id')
+							.andOn('proxy_host.is_deleted', '=', 0);
+					})
 					.where('access_list.is_deleted', 0)
 					.andWhere('access_list.id', data.id)
+					.groupBy('access_list.id')
 					.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
 					.first();
 
@@ -269,7 +273,7 @@ const internalAccessList = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
@@ -296,7 +300,7 @@ const internalAccessList = {
 				return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -373,7 +377,10 @@ const internalAccessList = {
 				let query = accessListModel
 					.query()
 					.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
-					.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
+					.leftJoin('proxy_host', function() {
+						this.on('proxy_host.access_list_id', '=', 'access_list.id')
+							.andOn('proxy_host.is_deleted', '=', 0);
+					})
 					.where('access_list.is_deleted', 0)
 					.groupBy('access_list.id')
 					.allowGraph('[owner,items,clients]')
@@ -501,8 +508,13 @@ const internalAccessList = {
 								if (typeof item.password !== 'undefined' && item.password.length) {
 									logger.info('Adding: ' + item.username);
 
-									utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
-										.then((/*result*/) => {
+									utils.execFile('openssl', ['passwd', '-apr1', item.password])
+										.then((res) => {
+											try {
+												fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'});
+											} catch (err) {
+												reject(err);
+											}
 											next();
 										})
 										.catch((err) => {
diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js
index cb48261b4..60bdd2efa 100644
--- a/backend/internal/audit-log.js
+++ b/backend/internal/audit-log.js
@@ -1,5 +1,6 @@
-const error         = require('../lib/error');
-const auditLogModel = require('../models/audit-log');
+const error            = require('../lib/error');
+const auditLogModel    = require('../models/audit-log');
+const {castJsonIfNeed} = require('../lib/helpers');
 
 const internalAuditLog = {
 
@@ -22,9 +23,9 @@ const internalAuditLog = {
 					.allowGraph('[user]');
 
 				// Query is used for searching
-				if (typeof search_query === 'string') {
+				if (typeof search_query === 'string' && search_query.length > 0) {
 					query.where(function () {
-						this.where('meta', 'like', '%' + search_query + '%');
+						this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%');
 					});
 				}
 
diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js
index 291056caa..f2e845a24 100644
--- a/backend/internal/certificate.js
+++ b/backend/internal/certificate.js
@@ -3,27 +3,29 @@ const fs               = require('fs');
 const https            = require('https');
 const tempWrite        = require('temp-write');
 const moment           = require('moment');
+const archiver         = require('archiver');
+const path             = require('path');
+const { isArray }      = require('lodash');
 const logger           = require('../logger').ssl;
 const config           = require('../lib/config');
 const error            = require('../lib/error');
 const utils            = require('../lib/utils');
+const certbot          = require('../lib/certbot');
 const certificateModel = require('../models/certificate');
 const tokenModel       = require('../models/token');
 const dnsPlugins       = require('../global/certbot-dns-plugins.json');
 const internalAuditLog = require('./audit-log');
 const internalNginx    = require('./nginx');
 const internalHost     = require('./host');
-const certbot          = require('../lib/certbot');
-const archiver         = require('archiver');
-const path             = require('path');
-const { isArray }      = require('lodash');
+
 
 const letsencryptStaging = config.useLetsencryptStaging();
+const letsencryptServer  = config.useLetsencryptServer();
 const letsencryptConfig  = '/etc/letsencrypt.ini';
 const certbotCommand     = 'certbot';
 
 function omissions() {
-	return ['is_deleted'];
+	return ['is_deleted', 'owner.is_deleted'];
 }
 
 const internalCertificate = {
@@ -207,6 +209,7 @@ const internalCertificate = {
 										.patchAndFetchById(certificate.id, {
 											expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
 										})
+										.then(utils.omitRow(omissions()))
 										.then((saved_row) => {
 											// Add cert data for audit log
 											saved_row.meta = _.assign({}, saved_row.meta, {
@@ -310,6 +313,9 @@ const internalCertificate = {
 					.where('is_deleted', 0)
 					.andWhere('id', data.id)
 					.allowGraph('[owner]')
+					.allowGraph('[proxy_hosts]')
+					.allowGraph('[redirection_hosts]')
+					.allowGraph('[dead_hosts]')
 					.first();
 
 				if (access_data.permission_visibility !== 'all') {
@@ -323,7 +329,7 @@ const internalCertificate = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				// Custom omissions
@@ -412,7 +418,7 @@ const internalCertificate = {
 				return internalCertificate.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -461,6 +467,9 @@ const internalCertificate = {
 					.where('is_deleted', 0)
 					.groupBy('id')
 					.allowGraph('[owner]')
+					.allowGraph('[proxy_hosts]')
+					.allowGraph('[redirection_hosts]')
+					.allowGraph('[dead_hosts]')
 					.orderBy('nice_name', 'ASC');
 
 				if (access_data.permission_visibility !== 'all') {
@@ -730,29 +739,29 @@ const internalCertificate = {
 
 		return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
 			.then((result) => {
+				// Examples:
+				// subject=CN = *.jc21.com
 				// subject=CN = something.example.com
 				const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
 				const match = regex.exec(result);
-
-				if (typeof match[1] === 'undefined') {
-					throw new error.ValidationError('Could not determine subject from certificate: ' + result);
+				if (match && typeof match[1] !== 'undefined') {
+					certData['cn'] = match[1];
 				}
-
-				certData['cn'] = match[1];
 			})
 			.then(() => {
 				return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
 			})
+
 			.then((result) => {
+				// Examples:
 				// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
+				// issuer=C = US, O = Let's Encrypt, CN = E5
+				// issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA
 				const regex = /^(?:issuer=)?(.*)$/gim;
 				const match = regex.exec(result);
-
-				if (typeof match[1] === 'undefined') {
-					throw new error.ValidationError('Could not determine issuer from certificate: ' + result);
+				if (match && typeof match[1] !== 'undefined') {
+					certData['issuer'] = match[1];
 				}
-
-				certData['issuer'] = match[1];
 			})
 			.then(() => {
 				return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
@@ -827,17 +836,18 @@ const internalCertificate = {
 	requestLetsEncryptSsl: (certificate) => {
 		logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
 
-		const cmd = certbotCommand + ' certonly ' +
-			'--config "' + letsencryptConfig + '" ' +
+		const cmd = `${certbotCommand} certonly ` +
+			`--config '${letsencryptConfig}' ` +
 			'--work-dir "/tmp/letsencrypt-lib" ' +
 			'--logs-dir "/tmp/letsencrypt-log" ' +
-			'--cert-name "npm-' + certificate.id + '" ' +
+			`--cert-name "npm-${certificate.id}" ` +
 			'--agree-tos ' +
 			'--authenticator webroot ' +
-			'--email "' + certificate.meta.letsencrypt_email + '" ' +
+			`--email '${certificate.meta.letsencrypt_email}' ` +
 			'--preferred-challenges "dns,http" ' +
-			'--domains "' + certificate.domain_names.join(',') + '" ' +
-			(letsencryptStaging ? '--staging' : '');
+			`--domains "${certificate.domain_names.join(',')}" ` +
+			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+			(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
 
 		logger.info('Command:', cmd);
 
@@ -868,25 +878,26 @@ const internalCertificate = {
 		const hasConfigArg = certificate.meta.dns_provider !== 'route53';
 
 		let mainCmd = certbotCommand + ' certonly ' +
-			'--config "' + letsencryptConfig + '" ' +
+			`--config '${letsencryptConfig}' ` +
 			'--work-dir "/tmp/letsencrypt-lib" ' +
 			'--logs-dir "/tmp/letsencrypt-log" ' +
-			'--cert-name "npm-' + certificate.id + '" ' +
+			`--cert-name 'npm-${certificate.id}' ` +
 			'--agree-tos ' +
-			'--email "' + certificate.meta.letsencrypt_email + '" ' +
-			'--domains "' + certificate.domain_names.join(',') + '" ' +
-			'--authenticator ' + dnsPlugin.full_plugin_name + ' ' +
+			`--email '${certificate.meta.letsencrypt_email}' ` +
+			`--domains '${certificate.domain_names.join(',')}' ` +
+			`--authenticator '${dnsPlugin.full_plugin_name}' ` +
 			(
 				hasConfigArg
-					? '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
+					? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' `
 					: ''
 			) +
 			(
 				certificate.meta.propagation_seconds !== undefined
-					? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
+					? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' `
 					: ''
 			) +
-			(letsencryptStaging ? ' --staging' : '');
+			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+			(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
 
 		// Prepend the path to the credentials file as an environment variable
 		if (certificate.meta.dns_provider === 'route53') {
@@ -963,14 +974,15 @@ const internalCertificate = {
 		logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
 
 		const cmd = certbotCommand + ' renew --force-renewal ' +
-			'--config "' + letsencryptConfig + '" ' +
+			`--config '${letsencryptConfig}' ` +
 			'--work-dir "/tmp/letsencrypt-lib" ' +
 			'--logs-dir "/tmp/letsencrypt-log" ' +
-			'--cert-name "npm-' + certificate.id + '" ' +
+			`--cert-name 'npm-${certificate.id}' ` +
 			'--preferred-challenges "dns,http" ' +
 			'--no-random-sleep-on-renew ' +
 			'--disable-hook-validation ' +
-			(letsencryptStaging ? '--staging' : '');
+			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+			(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
 
 		logger.info('Command:', cmd);
 
@@ -995,13 +1007,14 @@ const internalCertificate = {
 		logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
 
 		let mainCmd = certbotCommand + ' renew --force-renewal ' +
-			'--config "' + letsencryptConfig + '" ' +
+			`--config "${letsencryptConfig}" ` +
 			'--work-dir "/tmp/letsencrypt-lib" ' +
 			'--logs-dir "/tmp/letsencrypt-log" ' +
-			'--cert-name "npm-' + certificate.id + '" ' +
+			`--cert-name 'npm-${certificate.id}' ` +
 			'--disable-hook-validation ' +
 			'--no-random-sleep-on-renew ' +
-			(letsencryptStaging ? ' --staging' : '');
+			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+			(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
 
 		// Prepend the path to the credentials file as an environment variable
 		if (certificate.meta.dns_provider === 'route53') {
@@ -1027,12 +1040,13 @@ const internalCertificate = {
 		logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
 
 		const mainCmd = certbotCommand + ' revoke ' +
-			'--config "' + letsencryptConfig + '" ' +
+			`--config '${letsencryptConfig}' ` +
 			'--work-dir "/tmp/letsencrypt-lib" ' +
 			'--logs-dir "/tmp/letsencrypt-log" ' +
-			'--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' +
+			`--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` +
 			'--delete-after-revoke ' +
-			(letsencryptStaging ? '--staging' : '');
+			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+			(letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
 
 		// Don't fail command if file does not exist
 		const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js
index 2a6258e96..6bbdf61be 100644
--- a/backend/internal/dead-host.js
+++ b/backend/internal/dead-host.js
@@ -6,6 +6,7 @@ const internalHost        = require('./host');
 const internalNginx       = require('./nginx');
 const internalAuditLog    = require('./audit-log');
 const internalCertificate = require('./certificate');
+const {castJsonIfNeed}    = require('../lib/helpers');
 
 function omissions () {
 	return ['is_deleted'];
@@ -48,6 +49,12 @@ const internalDeadHost = {
 				data.owner_user_id = access.token.getUserId(1);
 				data               = internalHost.cleanSslHstsData(data);
 
+				// Fix for db field not having a default value
+				// for this optional field.
+				if (typeof data.advanced_config === 'undefined') {
+					data.advanced_config = '';
+				}
+
 				return deadHostModel
 					.query()
 					.insertAndFetch(data)
@@ -233,7 +240,7 @@ const internalDeadHost = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				// Custom omissions
@@ -257,7 +264,7 @@ const internalDeadHost = {
 				return internalDeadHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -305,7 +312,7 @@ const internalDeadHost = {
 				});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (row.enabled) {
 					throw new error.ValidationError('Host is already enabled');
@@ -351,7 +358,7 @@ const internalDeadHost = {
 				return internalDeadHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (!row.enabled) {
 					throw new error.ValidationError('Host is already disabled');
@@ -403,16 +410,16 @@ const internalDeadHost = {
 					.where('is_deleted', 0)
 					.groupBy('id')
 					.allowGraph('[owner,certificate]')
-					.orderBy('domain_names', 'ASC');
+					.orderBy(castJsonIfNeed('domain_names'), 'ASC');
 
 				if (access_data.permission_visibility !== 'all') {
 					query.andWhere('owner_user_id', access.token.getUserId(1));
 				}
 
 				// Query is used for searching
-				if (typeof search_query === 'string') {
+				if (typeof search_query === 'string' && search_query.length > 0) {
 					query.where(function () {
-						this.where('domain_names', 'like', '%' + search_query + '%');
+						this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%');
 					});
 				}
 
diff --git a/backend/internal/host.js b/backend/internal/host.js
index 58e1d09a4..52c6d2bda 100644
--- a/backend/internal/host.js
+++ b/backend/internal/host.js
@@ -2,6 +2,7 @@ const _                    = require('lodash');
 const proxyHostModel       = require('../models/proxy_host');
 const redirectionHostModel = require('../models/redirection_host');
 const deadHostModel        = require('../models/dead_host');
+const {castJsonIfNeed}     = require('../lib/helpers');
 
 const internalHost = {
 
@@ -17,7 +18,7 @@ const internalHost = {
 	cleanSslHstsData: function (data, existing_data) {
 		existing_data = existing_data === undefined ? {} : existing_data;
 
-		let combined_data = _.assign({}, existing_data, data);
+		const combined_data = _.assign({}, existing_data, data);
 
 		if (!combined_data.certificate_id) {
 			combined_data.ssl_forced    = false;
@@ -73,7 +74,7 @@ const internalHost = {
 	 * @returns {Promise}
 	 */
 	getHostsWithDomains: function (domain_names) {
-		let promises = [
+		const promises = [
 			proxyHostModel
 				.query()
 				.where('is_deleted', 0),
@@ -125,19 +126,19 @@ const internalHost = {
 	 * @returns {Promise}
 	 */
 	isHostnameTaken: function (hostname, ignore_type, ignore_id) {
-		let promises = [
+		const promises = [
 			proxyHostModel
 				.query()
 				.where('is_deleted', 0)
-				.andWhere('domain_names', 'like', '%' + hostname + '%'),
+				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
 			redirectionHostModel
 				.query()
 				.where('is_deleted', 0)
-				.andWhere('domain_names', 'like', '%' + hostname + '%'),
+				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
 			deadHostModel
 				.query()
 				.where('is_deleted', 0)
-				.andWhere('domain_names', 'like', '%' + hostname + '%')
+				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%')
 		];
 
 		return Promise.all(promises)
diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js
index 77933e733..5f802c004 100644
--- a/backend/internal/nginx.js
+++ b/backend/internal/nginx.js
@@ -181,7 +181,9 @@ const internalNginx = {
 	 * @param   {Object}  host
 	 * @returns {Promise}
 	 */
-	generateConfig: (host_type, host) => {
+	generateConfig: (host_type, host_row) => {
+		// Prevent modifying the original object:
+		let host             = JSON.parse(JSON.stringify(host_row));
 		const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
 
 		if (config.debug()) {
diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js
index dbff1147d..32f2bc0dc 100644
--- a/backend/internal/proxy-host.js
+++ b/backend/internal/proxy-host.js
@@ -6,9 +6,10 @@ const internalHost        = require('./host');
 const internalNginx       = require('./nginx');
 const internalAuditLog    = require('./audit-log');
 const internalCertificate = require('./certificate');
+const {castJsonIfNeed}    = require('../lib/helpers');
 
 function omissions () {
-	return ['is_deleted'];
+	return ['is_deleted', 'owner.is_deleted'];
 }
 
 const internalProxyHost = {
@@ -48,6 +49,12 @@ const internalProxyHost = {
 				data.owner_user_id = access.token.getUserId(1);
 				data               = internalHost.cleanSslHstsData(data);
 
+				// Fix for db field not having a default value
+				// for this optional field.
+				if (typeof data.advanced_config === 'undefined') {
+					data.advanced_config = '';
+				}
+
 				return proxyHostModel
 					.query()
 					.insertAndFetch(data)
@@ -239,7 +246,7 @@ const internalProxyHost = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				row = internalHost.cleanRowCertificateMeta(row);
@@ -264,7 +271,7 @@ const internalProxyHost = {
 				return internalProxyHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -312,7 +319,7 @@ const internalProxyHost = {
 				});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (row.enabled) {
 					throw new error.ValidationError('Host is already enabled');
@@ -358,7 +365,7 @@ const internalProxyHost = {
 				return internalProxyHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (!row.enabled) {
 					throw new error.ValidationError('Host is already disabled');
@@ -410,16 +417,16 @@ const internalProxyHost = {
 					.where('is_deleted', 0)
 					.groupBy('id')
 					.allowGraph('[owner,access_list,certificate]')
-					.orderBy('domain_names', 'ASC');
+					.orderBy(castJsonIfNeed('domain_names'), 'ASC');
 
 				if (access_data.permission_visibility !== 'all') {
 					query.andWhere('owner_user_id', access.token.getUserId(1));
 				}
 
 				// Query is used for searching
-				if (typeof search_query === 'string') {
+				if (typeof search_query === 'string' && search_query.length > 0) {
 					query.where(function () {
-						this.where('domain_names', 'like', '%' + search_query + '%');
+						this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
 					});
 				}
 
diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js
index 775d94f3a..6a81b8662 100644
--- a/backend/internal/redirection-host.js
+++ b/backend/internal/redirection-host.js
@@ -6,6 +6,7 @@ const internalHost         = require('./host');
 const internalNginx        = require('./nginx');
 const internalAuditLog     = require('./audit-log');
 const internalCertificate  = require('./certificate');
+const {castJsonIfNeed}     = require('../lib/helpers');
 
 function omissions () {
 	return ['is_deleted'];
@@ -48,6 +49,12 @@ const internalRedirectionHost = {
 				data.owner_user_id = access.token.getUserId(1);
 				data               = internalHost.cleanSslHstsData(data);
 
+				// Fix for db field not having a default value
+				// for this optional field.
+				if (typeof data.advanced_config === 'undefined') {
+					data.advanced_config = '';
+				}
+
 				return redirectionHostModel
 					.query()
 					.insertAndFetch(data)
@@ -232,7 +239,7 @@ const internalRedirectionHost = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				row = internalHost.cleanRowCertificateMeta(row);
@@ -257,7 +264,7 @@ const internalRedirectionHost = {
 				return internalRedirectionHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -305,7 +312,7 @@ const internalRedirectionHost = {
 				});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (row.enabled) {
 					throw new error.ValidationError('Host is already enabled');
@@ -351,7 +358,7 @@ const internalRedirectionHost = {
 				return internalRedirectionHost.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (!row.enabled) {
 					throw new error.ValidationError('Host is already disabled');
@@ -403,16 +410,16 @@ const internalRedirectionHost = {
 					.where('is_deleted', 0)
 					.groupBy('id')
 					.allowGraph('[owner,certificate]')
-					.orderBy('domain_names', 'ASC');
+					.orderBy(castJsonIfNeed('domain_names'), 'ASC');
 
 				if (access_data.permission_visibility !== 'all') {
 					query.andWhere('owner_user_id', access.token.getUserId(1));
 				}
 
 				// Query is used for searching
-				if (typeof search_query === 'string') {
+				if (typeof search_query === 'string' && search_query.length > 0) {
 					query.where(function () {
-						this.where('domain_names', 'like', '%' + search_query + '%');
+						this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
 					});
 				}
 
diff --git a/backend/internal/stream.js b/backend/internal/stream.js
index a159cfdd3..4d49bc3aa 100644
--- a/backend/internal/stream.js
+++ b/backend/internal/stream.js
@@ -1,12 +1,15 @@
-const _                = require('lodash');
-const error            = require('../lib/error');
-const utils            = require('../lib/utils');
-const streamModel      = require('../models/stream');
-const internalNginx    = require('./nginx');
-const internalAuditLog = require('./audit-log');
+const _                   = require('lodash');
+const error               = require('../lib/error');
+const utils               = require('../lib/utils');
+const streamModel         = require('../models/stream');
+const internalNginx       = require('./nginx');
+const internalAuditLog    = require('./audit-log');
+const internalCertificate = require('./certificate');
+const internalHost        = require('./host');
+const {castJsonIfNeed}    = require('../lib/helpers');
 
 function omissions () {
-	return ['is_deleted'];
+	return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
 }
 
 const internalStream = {
@@ -17,6 +20,12 @@ const internalStream = {
 	 * @returns {Promise}
 	 */
 	create: (access, data) => {
+		const create_certificate = data.certificate_id === 'new';
+
+		if (create_certificate) {
+			delete data.certificate_id;
+		}
+
 		return access.can('streams:create', data)
 			.then((/*access_data*/) => {
 				// TODO: At this point the existing ports should have been checked
@@ -26,16 +35,44 @@ const internalStream = {
 					data.meta = {};
 				}
 
+				// streams aren't routed by domain name so don't store domain names in the DB
+				let data_no_domains = structuredClone(data);
+				delete data_no_domains.domain_names;
+
 				return streamModel
 					.query()
-					.insertAndFetch(data)
+					.insertAndFetch(data_no_domains)
 					.then(utils.omitRow(omissions()));
 			})
+			.then((row) => {
+				if (create_certificate) {
+					return internalCertificate.createQuickCertificate(access, data)
+						.then((cert) => {
+							// update host with cert id
+							return internalStream.update(access, {
+								id:             row.id,
+								certificate_id: cert.id
+							});
+						})
+						.then(() => {
+							return row;
+						});
+				} else {
+					return row;
+				}
+			})
+			.then((row) => {
+				// re-fetch with cert
+				return internalStream.get(access, {
+					id:     row.id,
+					expand: ['certificate', 'owner']
+				});
+			})
 			.then((row) => {
 				// Configure nginx
 				return internalNginx.configure(streamModel, 'stream', row)
 					.then(() => {
-						return internalStream.get(access, {id: row.id, expand: ['owner']});
+						return row;
 					});
 			})
 			.then((row) => {
@@ -59,6 +96,12 @@ const internalStream = {
 	 * @return {Promise}
 	 */
 	update: (access, data) => {
+		const create_certificate = data.certificate_id === 'new';
+
+		if (create_certificate) {
+			delete data.certificate_id;
+		}
+
 		return access.can('streams:update', data.id)
 			.then((/*access_data*/) => {
 				// TODO: at this point the existing streams should have been checked
@@ -70,16 +113,32 @@ const internalStream = {
 					throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
 				}
 
+				if (create_certificate) {
+					return internalCertificate.createQuickCertificate(access, {
+						domain_names: data.domain_names || row.domain_names,
+						meta:         _.assign({}, row.meta, data.meta)
+					})
+						.then((cert) => {
+							// update host with cert id
+							data.certificate_id = cert.id;
+						})
+						.then(() => {
+							return row;
+						});
+				} else {
+					return row;
+				}
+			})
+			.then((row) => {
+				// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
+				data = _.assign({}, {
+					domain_names: row.domain_names
+				}, data);
+
 				return streamModel
 					.query()
 					.patchAndFetchById(row.id, data)
 					.then(utils.omitRow(omissions()))
-					.then((saved_row) => {
-						return internalNginx.configure(streamModel, 'stream', saved_row)
-							.then(() => {
-								return internalStream.get(access, {id: row.id, expand: ['owner']});
-							});
-					})
 					.then((saved_row) => {
 						// Add to audit log
 						return internalAuditLog.add(access, {
@@ -92,6 +151,17 @@ const internalStream = {
 								return saved_row;
 							});
 					});
+			})
+			.then(() => {
+				return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
+					.then((row) => {
+						return internalNginx.configure(streamModel, 'stream', row)
+							.then((new_meta) => {
+								row.meta = new_meta;
+								row      = internalHost.cleanRowCertificateMeta(row);
+								return _.omit(row, omissions());
+							});
+					});
 			});
 	},
 
@@ -114,7 +184,7 @@ const internalStream = {
 					.query()
 					.where('is_deleted', 0)
 					.andWhere('id', data.id)
-					.allowGraph('[owner]')
+					.allowGraph('[owner,certificate]')
 					.first();
 
 				if (access_data.permission_visibility !== 'all') {
@@ -128,9 +198,10 @@ const internalStream = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
+				row = internalHost.cleanRowCertificateMeta(row);
 				// Custom omissions
 				if (typeof data.omit !== 'undefined' && data.omit !== null) {
 					row = _.omit(row, data.omit);
@@ -152,7 +223,7 @@ const internalStream = {
 				return internalStream.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 
@@ -196,14 +267,14 @@ const internalStream = {
 			.then(() => {
 				return internalStream.get(access, {
 					id:     data.id,
-					expand: ['owner']
+					expand: ['certificate', 'owner']
 				});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (row.enabled) {
-					throw new error.ValidationError('Host is already enabled');
+					throw new error.ValidationError('Stream is already enabled');
 				}
 
 				row.enabled = 1;
@@ -246,10 +317,10 @@ const internalStream = {
 				return internalStream.get(access, {id: data.id});
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				} else if (!row.enabled) {
-					throw new error.ValidationError('Host is already disabled');
+					throw new error.ValidationError('Stream is already disabled');
 				}
 
 				row.enabled = 0;
@@ -293,21 +364,21 @@ const internalStream = {
 	getAll: (access, expand, search_query) => {
 		return access.can('streams:list')
 			.then((access_data) => {
-				let query = streamModel
+				const query = streamModel
 					.query()
 					.where('is_deleted', 0)
 					.groupBy('id')
-					.allowGraph('[owner]')
-					.orderBy('incoming_port', 'ASC');
+					.allowGraph('[owner,certificate]')
+					.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
 
 				if (access_data.permission_visibility !== 'all') {
 					query.andWhere('owner_user_id', access.token.getUserId(1));
 				}
 
 				// Query is used for searching
-				if (typeof search_query === 'string') {
+				if (typeof search_query === 'string' && search_query.length > 0) {
 					query.where(function () {
-						this.where('incoming_port', 'like', '%' + search_query + '%');
+						this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`);
 					});
 				}
 
@@ -316,6 +387,13 @@ const internalStream = {
 				}
 
 				return query.then(utils.omitRows(omissions()));
+			})
+			.then((rows) => {
+				if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
+					return internalHost.cleanAllRowsCertificateMeta(rows);
+				}
+
+				return rows;
 			});
 	},
 
@@ -327,9 +405,9 @@ const internalStream = {
 	 * @returns {Promise}
 	 */
 	getCount: (user_id, visibility) => {
-		let query = streamModel
+		const query = streamModel
 			.query()
-			.count('id as count')
+			.count('id AS count')
 			.where('is_deleted', 0);
 
 		if (visibility !== 'all') {
diff --git a/backend/internal/token.js b/backend/internal/token.js
index ed9a45f82..0e6dec5e3 100644
--- a/backend/internal/token.js
+++ b/backend/internal/token.js
@@ -5,6 +5,8 @@ const authModel  = require('../models/auth');
 const helpers    = require('../lib/helpers');
 const TokenModel = require('../models/token');
 
+const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
+
 module.exports = {
 
 	/**
@@ -69,15 +71,15 @@ module.exports = {
 													};
 												});
 										} else {
-											throw new error.AuthError('Invalid password');
+											throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
 										}
 									});
 							} else {
-								throw new error.AuthError('No password auth for user');
+								throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
 							}
 						});
 				} else {
-					throw new error.AuthError('No relevant user found');
+					throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
 				}
 			});
 	},
diff --git a/backend/internal/user.js b/backend/internal/user.js
index a1d90447f..742ab65d3 100644
--- a/backend/internal/user.js
+++ b/backend/internal/user.js
@@ -194,7 +194,7 @@ const internalUser = {
 				return query.then(utils.omitRow(omissions()));
 			})
 			.then((row) => {
-				if (!row) {
+				if (!row || !row.id) {
 					throw new error.ItemNotFoundError(data.id);
 				}
 				// Custom omissions
diff --git a/backend/knexfile.js b/backend/knexfile.js
index 391ca0050..607552f6f 100644
--- a/backend/knexfile.js
+++ b/backend/knexfile.js
@@ -1,6 +1,6 @@
 module.exports = {
 	development: {
-		client:     'mysql',
+		client:     'mysql2',
 		migrations: {
 			tableName: 'migrations',
 			stub:      'lib/migrate_template.js',
@@ -9,7 +9,7 @@ module.exports = {
 	},
 
 	production: {
-		client:     'mysql',
+		client:     'mysql2',
 		migrations: {
 			tableName: 'migrations',
 			stub:      'lib/migrate_template.js',
diff --git a/backend/lib/access.js b/backend/lib/access.js
index 5b9ebc93c..0e658a656 100644
--- a/backend/lib/access.js
+++ b/backend/lib/access.js
@@ -10,7 +10,7 @@
 
 const _              = require('lodash');
 const logger         = require('../logger').access;
-const validator      = require('ajv');
+const Ajv            = require('ajv/dist/2020');
 const error          = require('./error');
 const userModel      = require('../models/user');
 const proxyHostModel = require('../models/proxy_host');
@@ -174,7 +174,6 @@ module.exports = function (token_string) {
 
 		let schema = {
 			$id:                  'objects',
-			$schema:              'http://json-schema.org/draft-07/schema#',
 			description:          'Actor Properties',
 			type:                 'object',
 			additionalProperties: false,
@@ -251,7 +250,7 @@ module.exports = function (token_string) {
 						// Initialised, token decoded ok
 						return this.getObjectSchema(permission)
 							.then((objectSchema) => {
-								let data_schema = {
+								const data_schema = {
 									[permission]: {
 										data:                         data,
 										scope:                        Token.get('scope'),
@@ -267,24 +266,18 @@ module.exports = function (token_string) {
 								};
 
 								let permissionSchema = {
-									$schema:              'http://json-schema.org/draft-07/schema#',
 									$async:               true,
 									$id:                  'permissions',
+									type:                 'object',
 									additionalProperties: false,
 									properties:           {}
 								};
 
 								permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
 
-								// logger.info('objectSchema', JSON.stringify(objectSchema, null, 2));
-								// logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2));
-								// logger.info('data_schema', JSON.stringify(data_schema, null, 2));
-
-								let ajv = validator({
+								const ajv = new Ajv({
 									verbose:      true,
 									allErrors:    true,
-									format:       'full',
-									missingRefs:  'fail',
 									breakOnError: true,
 									coerceTypes:  true,
 									schemas:      [
diff --git a/backend/lib/access/permissions.json b/backend/lib/access/permissions.json
index 8480f9a1c..e7a82ece3 100644
--- a/backend/lib/access/permissions.json
+++ b/backend/lib/access/permissions.json
@@ -1,5 +1,4 @@
 {
-	"$schema": "http://json-schema.org/draft-07/schema#",
 	"$id": "perms",
 	"definitions": {
 		"view": {
diff --git a/backend/lib/access/roles.json b/backend/lib/access/roles.json
index 16b33b55b..c97313da8 100644
--- a/backend/lib/access/roles.json
+++ b/backend/lib/access/roles.json
@@ -1,5 +1,4 @@
 {
-	"$schema": "http://json-schema.org/draft-07/schema#",
 	"$id": "roles",
 	"definitions": {
 		"admin": {
diff --git a/backend/lib/config.js b/backend/lib/config.js
index a484fc5b2..23184f3e8 100644
--- a/backend/lib/config.js
+++ b/backend/lib/config.js
@@ -2,7 +2,10 @@ const fs      = require('fs');
 const NodeRSA = require('node-rsa');
 const logger  = require('../logger').global;
 
-const keysFile = '/data/keys.json';
+const keysFile         = '/data/keys.json';
+const mysqlEngine      = 'mysql2';
+const postgresEngine   = 'pg';
+const sqliteClientName = 'sqlite3';
 
 let instance = null;
 
@@ -14,7 +17,7 @@ const configure = () => {
 		let configData;
 		try {
 			configData = require(filename);
-		} catch (err) {
+		} catch (_) {
 			// do nothing
 		}
 
@@ -34,7 +37,7 @@ const configure = () => {
 		logger.info('Using MySQL configuration');
 		instance = {
 			database: {
-				engine:   'mysql',
+				engine:   mysqlEngine,
 				host:     envMysqlHost,
 				port:     process.env.DB_MYSQL_PORT || 3306,
 				user:     envMysqlUser,
@@ -46,13 +49,33 @@ const configure = () => {
 		return;
 	}
 
+	const envPostgresHost = process.env.DB_POSTGRES_HOST || null;
+	const envPostgresUser = process.env.DB_POSTGRES_USER || null;
+	const envPostgresName = process.env.DB_POSTGRES_NAME || null;
+	if (envPostgresHost && envPostgresUser && envPostgresName) {
+		// we have enough postgres creds to go with postgres
+		logger.info('Using Postgres configuration');
+		instance = {
+			database: {
+				engine:   postgresEngine,
+				host:     envPostgresHost,
+				port:     process.env.DB_POSTGRES_PORT || 5432,
+				user:     envPostgresUser,
+				password: process.env.DB_POSTGRES_PASSWORD,
+				name:     envPostgresName,
+			},
+			keys: getKeys(),
+		};
+		return;
+	}
+
 	const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite';
 	logger.info(`Using Sqlite: ${envSqliteFile}`);
 	instance = {
 		database: {
 			engine: 'knex-native',
 			knex:   {
-				client:     'sqlite3',
+				client:     sqliteClientName,
 				connection: {
 					filename: envSqliteFile
 				},
@@ -143,7 +166,27 @@ module.exports = {
 	 */
 	isSqlite: function () {
 		instance === null && configure();
-		return instance.database.knex && instance.database.knex.client === 'sqlite3';
+		return instance.database.knex && instance.database.knex.client === sqliteClientName;
+	},
+
+	/**
+	 * Is this a mysql configuration?
+	 *
+	 * @returns {boolean}
+	 */
+	isMysql: function () {
+		instance === null && configure();
+		return instance.database.engine === mysqlEngine;
+	},
+	
+	/**
+		 * Is this a postgres configuration?
+		 *
+		 * @returns {boolean}
+		 */
+	isPostgres: function () {
+		instance === null && configure();
+		return instance.database.engine === postgresEngine;
 	},
 
 	/**
@@ -180,5 +223,15 @@ module.exports = {
 	 */
 	useLetsencryptStaging: function () {
 		return !!process.env.LE_STAGING;
+	},
+
+	/**
+	 * @returns {string|null}
+	 */
+	useLetsencryptServer: function () {
+		if (process.env.LE_SERVER) {
+			return process.env.LE_SERVER;
+		}
+		return null;
 	}
 };
diff --git a/backend/lib/express/cors.js b/backend/lib/express/cors.js
index c9befeec8..6d5b8b5fb 100644
--- a/backend/lib/express/cors.js
+++ b/backend/lib/express/cors.js
@@ -1,40 +1,16 @@
-const validator = require('../validator');
-
 module.exports = function (req, res, next) {
-
 	if (req.headers.origin) {
-
-		const originSchema = {
-			oneOf: [
-				{
-					type:    'string',
-					pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
-				},
-				{
-					type:    'string',
-					pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$'
-				}
-			]
-		};
-
-		// very relaxed validation....
-		validator(originSchema, req.headers.origin)
-			.then(function () {
-				res.set({
-					'Access-Control-Allow-Origin':      req.headers.origin,
-					'Access-Control-Allow-Credentials': true,
-					'Access-Control-Allow-Methods':     'OPTIONS, GET, POST',
-					'Access-Control-Allow-Headers':     'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
-					'Access-Control-Max-Age':           5 * 60,
-					'Access-Control-Expose-Headers':    'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
-				});
-				next();
-			})
-			.catch(next);
-
+		res.set({
+			'Access-Control-Allow-Origin':      req.headers.origin,
+			'Access-Control-Allow-Credentials': true,
+			'Access-Control-Allow-Methods':     'OPTIONS, GET, POST',
+			'Access-Control-Allow-Headers':     'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
+			'Access-Control-Max-Age':           5 * 60,
+			'Access-Control-Expose-Headers':    'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
+		});
+		next();
 	} else {
 		// No origin
 		next();
 	}
-
 };
diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js
index e38be991e..ad3df3c27 100644
--- a/backend/lib/helpers.js
+++ b/backend/lib/helpers.js
@@ -1,4 +1,6 @@
-const moment = require('moment');
+const moment       = require('moment');
+const {isPostgres} = require('./config');
+const {ref}        = require('objection');
 
 module.exports = {
 
@@ -27,6 +29,34 @@ module.exports = {
 		}
 
 		return null;
+	},
+
+	convertIntFieldsToBool: function (obj, fields) {
+		fields.forEach(function (field) {
+			if (typeof obj[field] !== 'undefined') {
+				obj[field] = obj[field] === 1;
+			}
+		});
+		return obj;
+	},
+
+	convertBoolFieldsToInt: function (obj, fields) {
+		fields.forEach(function (field) {
+			if (typeof obj[field] !== 'undefined') {
+				obj[field] = obj[field] ? 1 : 0;
+			}
+		});
+		return obj;
+	},
+
+	/**
+	 * Casts a column to json if using postgres
+	 *
+	 * @param {string} colName
+	 * @returns {string|Objection.ReferenceBuilder}
+	 */
+	castJsonIfNeed: function (colName) {
+		return isPostgres() ? ref(colName).castText() : colName;
 	}
 
 };
diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js
index 3f51b5969..fb31e64c6 100644
--- a/backend/lib/validator/api.js
+++ b/backend/lib/validator/api.js
@@ -1,13 +1,12 @@
-const error  = require('../error');
-const path   = require('path');
-const parser = require('json-schema-ref-parser');
+const Ajv   = require('ajv/dist/2020');
+const error = require('../error');
 
-const ajv = require('ajv')({
-	verbose:        true,
-	validateSchema: true,
-	allErrors:      false,
-	format:         'full',
-	coerceTypes:    true
+const ajv = new Ajv({
+	verbose:         true,
+	allErrors:       true,
+	allowUnionTypes: true,
+	strict:          false,
+	coerceTypes:     true,
 });
 
 /**
@@ -17,12 +16,18 @@ const ajv = require('ajv')({
  */
 function apiValidator (schema, payload/*, description*/) {
 	return new Promise(function Promise_apiValidator (resolve, reject) {
+		if (schema === null) {
+			reject(new error.ValidationError('Schema is undefined'));
+			return;
+		}
+
 		if (typeof payload === 'undefined') {
 			reject(new error.ValidationError('Payload is undefined'));
+			return;
 		}
 
-		let validate = ajv.compile(schema);
-		let valid    = validate(payload);
+		const validate = ajv.compile(schema);
+		const valid    = validate(payload);
 
 		if (valid && !validate.errors) {
 			resolve(payload);
@@ -35,11 +40,4 @@ function apiValidator (schema, payload/*, description*/) {
 	});
 }
 
-apiValidator.loadSchemas = parser
-	.dereference(path.resolve('schema/index.json'))
-	.then((schema) => {
-		ajv.addSchema(schema);
-		return schema;
-	});
-
 module.exports = apiValidator;
diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js
index d09c9be5f..c6d240967 100644
--- a/backend/lib/validator/index.js
+++ b/backend/lib/validator/index.js
@@ -1,17 +1,17 @@
-const _           = require('lodash');
-const error       = require('../error');
-const definitions = require('../../schema/definitions.json');
+const _                 = require('lodash');
+const Ajv               = require('ajv/dist/2020');
+const error             = require('../error');
+const commonDefinitions = require('../../schema/common.json');
 
 RegExp.prototype.toJSON = RegExp.prototype.toString;
 
-const ajv = require('ajv')({
-	verbose:     true,
-	allErrors:   true,
-	format:      'full',  // strict regexes for format checks
-	coerceTypes: true,
-	schemas:     [
-		definitions
-	]
+const ajv = new Ajv({
+	verbose:         true,
+	allErrors:       true,
+	allowUnionTypes: true,
+	coerceTypes:     true,
+	strict:          false,
+	schemas:         [commonDefinitions]
 });
 
 /**
@@ -27,23 +27,19 @@ function validator (schema, payload) {
 		} else {
 			try {
 				let validate = ajv.compile(schema);
+				let valid    = validate(payload);
 
-				let valid = validate(payload);
 				if (valid && !validate.errors) {
 					resolve(_.cloneDeep(payload));
 				} else {
 					let message = ajv.errorsText(validate.errors);
 					reject(new error.InternalValidationError(message));
 				}
-
 			} catch (err) {
 				reject(err);
 			}
-
 		}
-
 	});
-
 }
 
 module.exports = validator;
diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js
new file mode 100644
index 000000000..5f47b18ec
--- /dev/null
+++ b/backend/migrations/20240427161436_stream_ssl.js
@@ -0,0 +1,38 @@
+const migrate_name = 'stream_ssl';
+const logger       = require('../logger').migrate;
+
+/**
+ * Migrate
+ *
+ * @see http://knexjs.org/#Schema
+ *
+ * @param   {Object} knex
+ * @returns {Promise}
+ */
+exports.up = function (knex) {
+	logger.info('[' + migrate_name + '] Migrating Up...');
+
+	return knex.schema.table('stream', (table) => {
+		table.integer('certificate_id').notNull().unsigned().defaultTo(0);
+	})
+		.then(function () {
+			logger.info('[' + migrate_name + '] stream Table altered');
+		});
+};
+
+/**
+ * Undo Migrate
+ *
+ * @param   {Object} knex
+ * @returns {Promise}
+ */
+exports.down = function (knex) {
+	logger.info('[' + migrate_name + '] Migrating Down...');
+
+	return knex.schema.table('stream', (table) => {
+		table.dropColumn('certificate_id');
+	})
+		.then(function () {
+			logger.info('[' + migrate_name + '] stream Table altered');
+		});
+};
diff --git a/backend/models/access_list.js b/backend/models/access_list.js
index fbf9bda77..959df05f3 100644
--- a/backend/models/access_list.js
+++ b/backend/models/access_list.js
@@ -2,6 +2,7 @@
 // http://vincit.github.io/objection.js/
 
 const db               = require('../db');
+const helpers          = require('../lib/helpers');
 const Model            = require('objection').Model;
 const User             = require('./user');
 const AccessListAuth   = require('./access_list_auth');
@@ -10,6 +11,12 @@ const now              = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'satisfy_any',
+	'pass_auth',
+];
+
 class AccessList extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -25,6 +32,16 @@ class AccessList extends Model {
 		this.modified_on = now();
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'AccessList';
 	}
diff --git a/backend/models/auth.js b/backend/models/auth.js
index 2ee431975..469e96bf4 100644
--- a/backend/models/auth.js
+++ b/backend/models/auth.js
@@ -1,14 +1,19 @@
 // Objection Docs:
 // http://vincit.github.io/objection.js/
 
-const bcrypt = require('bcrypt');
-const db     = require('../db');
-const Model  = require('objection').Model;
-const User   = require('./user');
-const now    = require('./now_helper');
+const bcrypt  = require('bcrypt');
+const db      = require('../db');
+const helpers = require('../lib/helpers');
+const Model   = require('objection').Model;
+const User    = require('./user');
+const now     = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+];
+
 function encryptPassword () {
 	/* jshint -W040 */
 	let _this = this;
@@ -41,6 +46,16 @@ class Auth extends Model {
 		return encryptPassword.apply(this, queryContext);
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	/**
 	 * Verify a plain password against the encrypted password
 	 *
diff --git a/backend/models/certificate.js b/backend/models/certificate.js
index 4f0f2ef64..d4ea21ad5 100644
--- a/backend/models/certificate.js
+++ b/backend/models/certificate.js
@@ -1,13 +1,17 @@
 // Objection Docs:
 // http://vincit.github.io/objection.js/
 
-const db    = require('../db');
-const Model = require('objection').Model;
-const User  = require('./user');
-const now   = require('./now_helper');
+const db      = require('../db');
+const helpers = require('../lib/helpers');
+const Model   = require('objection').Model;
+const now     = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+];
+
 class Certificate extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -40,6 +44,16 @@ class Certificate extends Model {
 		}
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'Certificate';
 	}
@@ -53,6 +67,11 @@ class Certificate extends Model {
 	}
 
 	static get relationMappings () {
+		const ProxyHost       = require('./proxy_host');
+		const DeadHost        = require('./dead_host');
+		const User            = require('./user');
+		const RedirectionHost = require('./redirection_host');
+
 		return {
 			owner: {
 				relation:   Model.HasOneRelation,
@@ -64,6 +83,39 @@ class Certificate extends Model {
 				modify: function (qb) {
 					qb.where('user.is_deleted', 0);
 				}
+			},
+			proxy_hosts: {
+				relation:   Model.HasManyRelation,
+				modelClass: ProxyHost,
+				join:       {
+					from: 'certificate.id',
+					to:   'proxy_host.certificate_id'
+				},
+				modify: function (qb) {
+					qb.where('proxy_host.is_deleted', 0);
+				}
+			},
+			dead_hosts: {
+				relation:   Model.HasManyRelation,
+				modelClass: DeadHost,
+				join:       {
+					from: 'certificate.id',
+					to:   'dead_host.certificate_id'
+				},
+				modify: function (qb) {
+					qb.where('dead_host.is_deleted', 0);
+				}
+			},
+			redirection_hosts: {
+				relation:   Model.HasManyRelation,
+				modelClass: RedirectionHost,
+				join:       {
+					from: 'certificate.id',
+					to:   'redirection_host.certificate_id'
+				},
+				modify: function (qb) {
+					qb.where('redirection_host.is_deleted', 0);
+				}
 			}
 		};
 	}
diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js
index 2e31043ae..3386caabf 100644
--- a/backend/models/dead_host.js
+++ b/backend/models/dead_host.js
@@ -2,6 +2,7 @@
 // http://vincit.github.io/objection.js/
 
 const db          = require('../db');
+const helpers     = require('../lib/helpers');
 const Model       = require('objection').Model;
 const User        = require('./user');
 const Certificate = require('./certificate');
@@ -9,6 +10,15 @@ const now         = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'ssl_forced',
+	'http2_support',
+	'enabled',
+	'hsts_enabled',
+	'hsts_subdomains',
+];
+
 class DeadHost extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -36,6 +46,16 @@ class DeadHost extends Model {
 		}
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'DeadHost';
 	}
diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js
index d84181cf1..07aa5dd3c 100644
--- a/backend/models/proxy_host.js
+++ b/backend/models/proxy_host.js
@@ -2,6 +2,7 @@
 // http://vincit.github.io/objection.js/
 
 const db          = require('../db');
+const helpers     = require('../lib/helpers');
 const Model       = require('objection').Model;
 const User        = require('./user');
 const AccessList  = require('./access_list');
@@ -10,6 +11,18 @@ const now         = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'ssl_forced',
+	'caching_enabled',
+	'block_exploits',
+	'allow_websocket_upgrade',
+	'http2_support',
+	'enabled',
+	'hsts_enabled',
+	'hsts_subdomains',
+];
+
 class ProxyHost extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -37,6 +50,16 @@ class ProxyHost extends Model {
 		}
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'ProxyHost';
 	}
diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js
index c90a6de6c..801627916 100644
--- a/backend/models/redirection_host.js
+++ b/backend/models/redirection_host.js
@@ -3,6 +3,7 @@
 // http://vincit.github.io/objection.js/
 
 const db          = require('../db');
+const helpers     = require('../lib/helpers');
 const Model       = require('objection').Model;
 const User        = require('./user');
 const Certificate = require('./certificate');
@@ -10,6 +11,17 @@ const now         = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'enabled',
+	'preserve_path',
+	'ssl_forced',
+	'block_exploits',
+	'hsts_enabled',
+	'hsts_subdomains',
+	'http2_support',
+];
+
 class RedirectionHost extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -37,6 +49,16 @@ class RedirectionHost extends Model {
 		}
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'RedirectionHost';
 	}
diff --git a/backend/models/stream.js b/backend/models/stream.js
index 7d84d2c36..5d1cb6c1c 100644
--- a/backend/models/stream.js
+++ b/backend/models/stream.js
@@ -1,13 +1,19 @@
-// Objection Docs:
-// http://vincit.github.io/objection.js/
-
-const db    = require('../db');
-const Model = require('objection').Model;
-const User  = require('./user');
-const now   = require('./now_helper');
+const Model       = require('objection').Model;
+const db          = require('../db');
+const helpers     = require('../lib/helpers');
+const User        = require('./user');
+const Certificate = require('./certificate');
+const now         = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'enabled',
+	'tcp_forwarding',
+	'udp_forwarding',
+];
+
 class Stream extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -23,6 +29,16 @@ class Stream extends Model {
 		this.modified_on = now();
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'Stream';
 	}
@@ -47,6 +63,17 @@ class Stream extends Model {
 				modify: function (qb) {
 					qb.where('user.is_deleted', 0);
 				}
+			},
+			certificate: {
+				relation:   Model.HasOneRelation,
+				modelClass: Certificate,
+				join:       {
+					from: 'stream.certificate_id',
+					to:   'certificate.id'
+				},
+				modify: function (qb) {
+					qb.where('certificate.is_deleted', 0);
+				}
 			}
 		};
 	}
diff --git a/backend/models/user.js b/backend/models/user.js
index 93489fefe..78fd3dd67 100644
--- a/backend/models/user.js
+++ b/backend/models/user.js
@@ -2,12 +2,18 @@
 // http://vincit.github.io/objection.js/
 
 const db             = require('../db');
+const helpers        = require('../lib/helpers');
 const Model          = require('objection').Model;
 const UserPermission = require('./user_permission');
 const now            = require('./now_helper');
 
 Model.knex(db);
 
+const boolFields = [
+	'is_deleted',
+	'is_disabled',
+];
+
 class User extends Model {
 	$beforeInsert () {
 		this.created_on  = now();
@@ -23,6 +29,16 @@ class User extends Model {
 		this.modified_on = now();
 	}
 
+	$parseDatabaseJson(json) {
+		json = super.$parseDatabaseJson(json);
+		return helpers.convertIntFieldsToBool(json, boolFields);
+	}
+
+	$formatDatabaseJson(json) {
+		json = helpers.convertBoolFieldsToInt(json, boolFields);
+		return super.$formatDatabaseJson(json);
+	}
+
 	static get name () {
 		return 'User';
 	}
diff --git a/backend/package.json b/backend/package.json
index b938c9a9c..30984a332 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -2,27 +2,28 @@
 	"name": "nginx-proxy-manager",
 	"version": "0.0.0",
 	"description": "A beautiful interface for creating Nginx endpoints",
-	"main": "js/index.js",
+	"main": "index.js",
 	"dependencies": {
-		"ajv": "^6.12.0",
+		"@apidevtools/json-schema-ref-parser": "^11.7.0",
+		"ajv": "^8.17.1",
 		"archiver": "^5.3.0",
 		"batchflow": "^0.4.0",
 		"bcrypt": "^5.0.0",
-		"body-parser": "^1.19.0",
+		"body-parser": "^1.20.3",
 		"compression": "^1.7.4",
-		"express": "^4.19.2",
+		"express": "^4.20.0",
 		"express-fileupload": "^1.1.9",
 		"gravatar": "^1.8.0",
-		"json-schema-ref-parser": "^8.0.0",
 		"jsonwebtoken": "^9.0.0",
 		"knex": "2.4.2",
 		"liquidjs": "10.6.1",
 		"lodash": "^4.17.21",
 		"moment": "^2.29.4",
-		"mysql": "^2.18.1",
+		"mysql2": "^3.11.1",
 		"node-rsa": "^1.0.8",
 		"objection": "3.0.1",
 		"path": "^0.12.7",
+		"pg": "^8.13.1",
 		"signale": "1.4.0",
 		"sqlite3": "5.1.6",
 		"temp-write": "^4.0.0"
@@ -34,9 +35,14 @@
 	"author": "Jamie Curnow <jc@jc21.com>",
 	"license": "MIT",
 	"devDependencies": {
+		"@apidevtools/swagger-parser": "^10.1.0",
+		"chalk": "4.1.2",
 		"eslint": "^8.36.0",
 		"eslint-plugin-align-assignments": "^1.1.2",
 		"nodemon": "^2.0.2",
 		"prettier": "^2.0.4"
+	},
+	"scripts": {
+		"validate-schema": "node validate-schema.js"
 	}
 }
diff --git a/backend/routes/api/audit-log.js b/backend/routes/audit-log.js
similarity index 73%
rename from backend/routes/api/audit-log.js
rename to backend/routes/audit-log.js
index 8a2490c3f..c68c7b35b 100644
--- a/backend/routes/api/audit-log.js
+++ b/backend/routes/audit-log.js
@@ -1,7 +1,7 @@
 const express          = require('express');
-const validator        = require('../../lib/validator');
-const jwtdecode        = require('../../lib/express/jwt-decode');
-const internalAuditLog = require('../../internal/audit-log');
+const validator        = require('../lib/validator');
+const jwtdecode        = require('../lib/express/jwt-decode');
+const internalAuditLog = require('../internal/audit-log');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -14,7 +14,7 @@ let router = express.Router({
  */
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -29,10 +29,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
diff --git a/backend/routes/api/main.js b/backend/routes/main.js
similarity index 90%
rename from backend/routes/api/main.js
rename to backend/routes/main.js
index 33cbbc21f..b97096d0e 100644
--- a/backend/routes/api/main.js
+++ b/backend/routes/main.js
@@ -1,6 +1,6 @@
 const express = require('express');
-const pjson   = require('../../package.json');
-const error   = require('../../lib/error');
+const pjson   = require('../package.json');
+const error   = require('../lib/error');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -43,7 +43,7 @@ router.use('/nginx/certificates', require('./nginx/certificates'));
  *
  * ALL /api/*
  */
-router.all(/(.+)/, function (req, res, next) {
+router.all(/(.+)/, function (req, _, next) {
 	req.params.page = req.params['0'];
 	next(new error.ItemNotFoundError(req.params.page));
 });
diff --git a/backend/routes/api/nginx/access_lists.js b/backend/routes/nginx/access_lists.js
similarity index 79%
rename from backend/routes/api/nginx/access_lists.js
rename to backend/routes/nginx/access_lists.js
index d55c3ae12..383751277 100644
--- a/backend/routes/api/nginx/access_lists.js
+++ b/backend/routes/nginx/access_lists.js
@@ -1,8 +1,9 @@
 const express            = require('express');
-const validator          = require('../../../lib/validator');
-const jwtdecode          = require('../../../lib/express/jwt-decode');
-const internalAccessList = require('../../../internal/access-list');
-const apiValidator       = require('../../../lib/validator/api');
+const validator          = require('../../lib/validator');
+const jwtdecode          = require('../../lib/express/jwt-decode');
+const apiValidator       = require('../../lib/validator/api');
+const internalAccessList = require('../../internal/access-list');
+const schema             = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -30,10 +31,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +57,7 @@ router
 	 * Create a new access-list
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/access-lists', 'post'), req.body)
 			.then((payload) => {
 				return internalAccessList.create(res.locals.access, payload);
 			})
@@ -74,7 +75,7 @@ router
  */
 router
 	.route('/:list_id')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -90,10 +91,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				list_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -119,7 +120,7 @@ router
 	 * Update and existing access-list
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/access-lists/{listID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = parseInt(req.params.list_id, 10);
 				return internalAccessList.update(res.locals.access, payload);
diff --git a/backend/routes/api/nginx/certificates.js b/backend/routes/nginx/certificates.js
similarity index 80%
rename from backend/routes/api/nginx/certificates.js
rename to backend/routes/nginx/certificates.js
index ffdfb515d..bf47c03fc 100644
--- a/backend/routes/api/nginx/certificates.js
+++ b/backend/routes/nginx/certificates.js
@@ -1,8 +1,10 @@
 const express             = require('express');
-const validator           = require('../../../lib/validator');
-const jwtdecode           = require('../../../lib/express/jwt-decode');
-const internalCertificate = require('../../../internal/certificate');
-const apiValidator        = require('../../../lib/validator/api');
+const error               = require('../../lib/error');
+const validator           = require('../../lib/validator');
+const jwtdecode           = require('../../lib/express/jwt-decode');
+const apiValidator        = require('../../lib/validator/api');
+const internalCertificate = require('../../internal/certificate');
+const schema              = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -15,7 +17,7 @@ let router = express.Router({
  */
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -30,10 +32,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +58,7 @@ router
 	 * Create a new certificate
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/certificates', 'post'), req.body)
 			.then((payload) => {
 				req.setTimeout(900000); // 15 minutes timeout
 				return internalCertificate.create(res.locals.access, payload);
@@ -75,17 +77,22 @@ router
  */
 router
 	.route('/test-http')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
 
-/**
- * GET /api/nginx/certificates/test-http
- *
- * Test HTTP challenge for domains
- */
+	/**
+	 * GET /api/nginx/certificates/test-http
+	 *
+	 * Test HTTP challenge for domains
+	 */
 	.get((req, res, next) => {
+		if (req.query.domains === undefined) {
+			next(new error.ValidationError('Domains are required as query parameters'));
+			return;
+		}
+
 		internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
 			.then((result) => {
 				res.status(200)
@@ -101,7 +108,7 @@ router
  */
 router
 	.route('/:certificate_id')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -117,10 +124,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				certificate_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -140,24 +147,6 @@ router
 			.catch(next);
 	})
 
-	/**
-	 * PUT /api/nginx/certificates/123
-	 *
-	 * Update and existing certificate
-	 */
-	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body)
-			.then((payload) => {
-				payload.id = parseInt(req.params.certificate_id, 10);
-				return internalCertificate.update(res.locals.access, payload);
-			})
-			.then((result) => {
-				res.status(200)
-					.send(result);
-			})
-			.catch(next);
-	})
-
 	/**
 	 * DELETE /api/nginx/certificates/123
 	 *
@@ -179,7 +168,7 @@ router
  */
 router
 	.route('/:certificate_id/upload')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -213,7 +202,7 @@ router
  */
 router
 	.route('/:certificate_id/renew')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -270,7 +259,7 @@ router
  */
 router
 	.route('/validate')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
diff --git a/backend/routes/api/nginx/dead_hosts.js b/backend/routes/nginx/dead_hosts.js
similarity index 83%
rename from backend/routes/api/nginx/dead_hosts.js
rename to backend/routes/nginx/dead_hosts.js
index 08b58f2de..83b377653 100644
--- a/backend/routes/api/nginx/dead_hosts.js
+++ b/backend/routes/nginx/dead_hosts.js
@@ -1,8 +1,9 @@
 const express          = require('express');
-const validator        = require('../../../lib/validator');
-const jwtdecode        = require('../../../lib/express/jwt-decode');
-const internalDeadHost = require('../../../internal/dead-host');
-const apiValidator     = require('../../../lib/validator/api');
+const validator        = require('../../lib/validator');
+const jwtdecode        = require('../../lib/express/jwt-decode');
+const apiValidator     = require('../../lib/validator/api');
+const internalDeadHost = require('../../internal/dead-host');
+const schema           = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -15,7 +16,7 @@ let router = express.Router({
  */
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -30,10 +31,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +57,7 @@ router
 	 * Create a new dead-host
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/dead-hosts', 'post'), req.body)
 			.then((payload) => {
 				return internalDeadHost.create(res.locals.access, payload);
 			})
@@ -90,10 +91,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				host_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -119,7 +120,7 @@ router
 	 * Update and existing dead-host
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/dead-hosts/{hostID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = parseInt(req.params.host_id, 10);
 				return internalDeadHost.update(res.locals.access, payload);
@@ -152,7 +153,7 @@ router
  */
 router
 	.route('/:host_id/enable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -176,7 +177,7 @@ router
  */
 router
 	.route('/:host_id/disable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
diff --git a/backend/routes/api/nginx/proxy_hosts.js b/backend/routes/nginx/proxy_hosts.js
similarity index 83%
rename from backend/routes/api/nginx/proxy_hosts.js
rename to backend/routes/nginx/proxy_hosts.js
index 6f933c3d3..3be4582a8 100644
--- a/backend/routes/api/nginx/proxy_hosts.js
+++ b/backend/routes/nginx/proxy_hosts.js
@@ -1,8 +1,9 @@
 const express           = require('express');
-const validator         = require('../../../lib/validator');
-const jwtdecode         = require('../../../lib/express/jwt-decode');
-const internalProxyHost = require('../../../internal/proxy-host');
-const apiValidator      = require('../../../lib/validator/api');
+const validator         = require('../../lib/validator');
+const jwtdecode         = require('../../lib/express/jwt-decode');
+const apiValidator      = require('../../lib/validator/api');
+const internalProxyHost = require('../../internal/proxy-host');
+const schema            = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -30,10 +31,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +57,7 @@ router
 	 * Create a new proxy-host
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/proxy-hosts', 'post'), req.body)
 			.then((payload) => {
 				return internalProxyHost.create(res.locals.access, payload);
 			})
@@ -90,10 +91,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				host_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -119,7 +120,7 @@ router
 	 * Update and existing proxy-host
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/proxy-hosts/{hostID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = parseInt(req.params.host_id, 10);
 				return internalProxyHost.update(res.locals.access, payload);
@@ -152,7 +153,7 @@ router
  */
 router
 	.route('/:host_id/enable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -176,7 +177,7 @@ router
  */
 router
 	.route('/:host_id/disable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
diff --git a/backend/routes/api/nginx/redirection_hosts.js b/backend/routes/nginx/redirection_hosts.js
similarity index 84%
rename from backend/routes/api/nginx/redirection_hosts.js
rename to backend/routes/nginx/redirection_hosts.js
index 4d44c1126..a46feb841 100644
--- a/backend/routes/api/nginx/redirection_hosts.js
+++ b/backend/routes/nginx/redirection_hosts.js
@@ -1,8 +1,9 @@
 const express                 = require('express');
-const validator               = require('../../../lib/validator');
-const jwtdecode               = require('../../../lib/express/jwt-decode');
-const internalRedirectionHost = require('../../../internal/redirection-host');
-const apiValidator            = require('../../../lib/validator/api');
+const validator               = require('../../lib/validator');
+const jwtdecode               = require('../../lib/express/jwt-decode');
+const apiValidator            = require('../../lib/validator/api');
+const internalRedirectionHost = require('../../internal/redirection-host');
+const schema                  = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -30,10 +31,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +57,7 @@ router
 	 * Create a new redirection-host
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/redirection-hosts', 'post'), req.body)
 			.then((payload) => {
 				return internalRedirectionHost.create(res.locals.access, payload);
 			})
@@ -90,10 +91,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				host_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -119,7 +120,7 @@ router
 	 * Update and existing redirection-host
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/redirection-hosts/{hostID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = parseInt(req.params.host_id, 10);
 				return internalRedirectionHost.update(res.locals.access, payload);
diff --git a/backend/routes/api/nginx/streams.js b/backend/routes/nginx/streams.js
similarity index 84%
rename from backend/routes/api/nginx/streams.js
rename to backend/routes/nginx/streams.js
index 5e3fc28fe..c033f2ef1 100644
--- a/backend/routes/api/nginx/streams.js
+++ b/backend/routes/nginx/streams.js
@@ -1,8 +1,9 @@
 const express        = require('express');
-const validator      = require('../../../lib/validator');
-const jwtdecode      = require('../../../lib/express/jwt-decode');
-const internalStream = require('../../../internal/stream');
-const apiValidator   = require('../../../lib/validator/api');
+const validator      = require('../../lib/validator');
+const jwtdecode      = require('../../lib/express/jwt-decode');
+const apiValidator   = require('../../lib/validator/api');
+const internalStream = require('../../internal/stream');
+const schema         = require('../../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -30,10 +31,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -56,7 +57,7 @@ router
 	 * Create a new stream
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/streams', 'post'), req.body)
 			.then((payload) => {
 				return internalStream.create(res.locals.access, payload);
 			})
@@ -90,10 +91,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				stream_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -119,7 +120,7 @@ router
 	 * Update and existing stream
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/nginx/streams/{streamID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = parseInt(req.params.stream_id, 10);
 				return internalStream.update(res.locals.access, payload);
@@ -152,7 +153,7 @@ router
  */
 router
 	.route('/:host_id/enable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -176,7 +177,7 @@ router
  */
 router
 	.route('/:host_id/disable')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
diff --git a/backend/routes/api/reports.js b/backend/routes/reports.js
similarity index 67%
rename from backend/routes/api/reports.js
rename to backend/routes/reports.js
index 9e2c98c89..98c6cf86d 100644
--- a/backend/routes/api/reports.js
+++ b/backend/routes/reports.js
@@ -1,6 +1,6 @@
 const express        = require('express');
-const jwtdecode      = require('../../lib/express/jwt-decode');
-const internalReport = require('../../internal/report');
+const jwtdecode      = require('../lib/express/jwt-decode');
+const internalReport = require('../internal/report');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -10,14 +10,14 @@ let router = express.Router({
 
 router
 	.route('/hosts')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 
 	/**
 	 * GET /reports/hosts
 	 */
-	.get(jwtdecode(), (req, res, next) => {
+	.get(jwtdecode(), (_, res, next) => {
 		internalReport.getHostsReport(res.locals.access)
 			.then((data) => {
 				res.status(200)
diff --git a/backend/routes/api/schema.js b/backend/routes/schema.js
similarity index 71%
rename from backend/routes/api/schema.js
rename to backend/routes/schema.js
index fc6bd5bdf..fc3e48b6c 100644
--- a/backend/routes/api/schema.js
+++ b/backend/routes/schema.js
@@ -1,8 +1,8 @@
-const express     = require('express');
-const swaggerJSON = require('../../doc/api.swagger.json');
-const PACKAGE     = require('../../package.json');
+const express = require('express');
+const schema  = require('../schema');
+const PACKAGE = require('../package.json');
 
-let router = express.Router({
+const router = express.Router({
 	caseSensitive: true,
 	strict:        true,
 	mergeParams:   true
@@ -10,14 +10,16 @@ let router = express.Router({
 
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 
 	/**
 	 * GET /schema
 	 */
-	.get((req, res/*, next*/) => {
+	.get(async (req, res) => {
+		let swaggerJSON = await schema.getCompiledSchema();
+
 		let proto = req.protocol;
 		if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && req.headers['x-forwarded-proto']) {
 			proto = req.headers['x-forwarded-proto'];
diff --git a/backend/routes/api/settings.js b/backend/routes/settings.js
similarity index 74%
rename from backend/routes/api/settings.js
rename to backend/routes/settings.js
index d08b2bf5c..dac4c3d1a 100644
--- a/backend/routes/api/settings.js
+++ b/backend/routes/settings.js
@@ -1,8 +1,9 @@
 const express         = require('express');
-const validator       = require('../../lib/validator');
-const jwtdecode       = require('../../lib/express/jwt-decode');
-const internalSetting = require('../../internal/setting');
-const apiValidator    = require('../../lib/validator/api');
+const validator       = require('../lib/validator');
+const jwtdecode       = require('../lib/express/jwt-decode');
+const apiValidator    = require('../lib/validator/api');
+const internalSetting = require('../internal/setting');
+const schema          = require('../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -15,7 +16,7 @@ let router = express.Router({
  */
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -25,7 +26,7 @@ router
 	 *
 	 * Retrieve all settings
 	 */
-	.get((req, res, next) => {
+	.get((_, res, next) => {
 		internalSetting.getAll(res.locals.access)
 			.then((rows) => {
 				res.status(200)
@@ -41,7 +42,7 @@ router
  */
 router
 	.route('/:setting_id')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -57,7 +58,8 @@ router
 			additionalProperties: false,
 			properties:           {
 				setting_id: {
-					$ref: 'definitions#/definitions/setting_id'
+					type:      'string',
+					minLength: 1
 				}
 			}
 		}, {
@@ -81,7 +83,7 @@ router
 	 * Update and existing setting
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/settings/{settingID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = req.params.setting_id;
 				return internalSetting.update(res.locals.access, payload);
diff --git a/backend/routes/api/tokens.js b/backend/routes/tokens.js
similarity index 70%
rename from backend/routes/api/tokens.js
rename to backend/routes/tokens.js
index a21f998ae..72d01d41d 100644
--- a/backend/routes/api/tokens.js
+++ b/backend/routes/tokens.js
@@ -1,7 +1,8 @@
 const express       = require('express');
-const jwtdecode     = require('../../lib/express/jwt-decode');
-const internalToken = require('../../internal/token');
-const apiValidator  = require('../../lib/validator/api');
+const jwtdecode     = require('../lib/express/jwt-decode');
+const apiValidator  = require('../lib/validator/api');
+const internalToken = require('../internal/token');
+const schema        = require('../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -11,7 +12,7 @@ let router = express.Router({
 
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 
@@ -39,11 +40,9 @@ router
 	 *
 	 * Create a new Token
 	 */
-	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body)
-			.then((payload) => {
-				return internalToken.getTokenFromEmail(payload);
-			})
+	.post(async (req, res, next) => {
+		apiValidator(schema.getValidationSchema('/tokens', 'post'), req.body)
+			.then(internalToken.getTokenFromEmail)
 			.then((data) => {
 				res.status(200)
 					.send(data);
diff --git a/backend/routes/api/users.js b/backend/routes/users.js
similarity index 79%
rename from backend/routes/api/users.js
rename to backend/routes/users.js
index 1c6bd0ad2..f8ce366c9 100644
--- a/backend/routes/api/users.js
+++ b/backend/routes/users.js
@@ -1,9 +1,10 @@
 const express      = require('express');
-const validator    = require('../../lib/validator');
-const jwtdecode    = require('../../lib/express/jwt-decode');
-const userIdFromMe = require('../../lib/express/user-id-from-me');
-const internalUser = require('../../internal/user');
-const apiValidator = require('../../lib/validator/api');
+const validator    = require('../lib/validator');
+const jwtdecode    = require('../lib/express/jwt-decode');
+const userIdFromMe = require('../lib/express/user-id-from-me');
+const internalUser = require('../internal/user');
+const apiValidator = require('../lib/validator/api');
+const schema       = require('../schema');
 
 let router = express.Router({
 	caseSensitive: true,
@@ -16,7 +17,7 @@ let router = express.Router({
  */
 router
 	.route('/')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -31,10 +32,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				},
 				query: {
-					$ref: 'definitions#/definitions/query'
+					$ref: 'common#/properties/query'
 				}
 			}
 		}, {
@@ -48,7 +49,11 @@ router
 				res.status(200)
 					.send(users);
 			})
-			.catch(next);
+			.catch((err) => {
+				console.log(err);
+				next(err);
+			});
+		//.catch(next);
 	})
 
 	/**
@@ -57,7 +62,7 @@ router
 	 * Create a new User
 	 */
 	.post((req, res, next) => {
-		apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/users', 'post'), req.body)
 			.then((payload) => {
 				return internalUser.create(res.locals.access, payload);
 			})
@@ -75,7 +80,7 @@ router
  */
 router
 	.route('/:user_id')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
@@ -92,10 +97,10 @@ router
 			additionalProperties: false,
 			properties:           {
 				user_id: {
-					$ref: 'definitions#/definitions/id'
+					$ref: 'common#/properties/id'
 				},
 				expand: {
-					$ref: 'definitions#/definitions/expand'
+					$ref: 'common#/properties/expand'
 				}
 			}
 		}, {
@@ -113,7 +118,10 @@ router
 				res.status(200)
 					.send(user);
 			})
-			.catch(next);
+			.catch((err) => {
+				console.log(err);
+				next(err);
+			});
 	})
 
 	/**
@@ -122,7 +130,7 @@ router
 	 * Update and existing user
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/users/{userID}', 'put'), req.body)
 			.then((payload) => {
 				payload.id = req.params.user_id;
 				return internalUser.update(res.locals.access, payload);
@@ -167,7 +175,7 @@ router
 	 * Update password for a user
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/users/{userID}/auth', 'put'), req.body)
 			.then((payload) => {
 				payload.id = req.params.user_id;
 				return internalUser.setPassword(res.locals.access, payload);
@@ -198,7 +206,7 @@ router
 	 * Set some or all permissions for a user
 	 */
 	.put((req, res, next) => {
-		apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body)
+		apiValidator(schema.getValidationSchema('/users/{userID}/permissions', 'put'), req.body)
 			.then((payload) => {
 				payload.id = req.params.user_id;
 				return internalUser.setPermissions(res.locals.access, payload);
@@ -217,7 +225,7 @@ router
  */
 router
 	.route('/:user_id/login')
-	.options((req, res) => {
+	.options((_, res) => {
 		res.sendStatus(204);
 	})
 	.all(jwtdecode())
diff --git a/backend/schema/common.json b/backend/schema/common.json
new file mode 100644
index 000000000..83de0143c
--- /dev/null
+++ b/backend/schema/common.json
@@ -0,0 +1,115 @@
+{
+	"$schema": "https://json-schema.org/draft/2020-12/schema",
+	"$id": "common",
+	"type": "object",
+	"properties": {
+		"id": {
+			"description": "Unique identifier",
+			"readOnly": true,
+			"type": "integer",
+			"minimum": 1
+		},
+		"expand": {
+			"anyOf": [
+				{
+					"type": "null"
+				},
+				{
+					"type": "array",
+					"minItems": 1,
+					"items": {
+						"type": "string"
+					}
+				}
+			]
+		},
+		"query": {
+			"anyOf": [
+				{
+					"type": "null"
+				},
+				{
+					"type": "string",
+					"minLength": 1,
+					"maxLength": 255
+				}
+			]
+		},
+		"created_on": {
+			"description": "Date and time of creation",
+			"readOnly": true,
+			"type": "string"
+		},
+		"modified_on": {
+			"description": "Date and time of last update",
+			"readOnly": true,
+			"type": "string"
+		},
+		"user_id": {
+			"description": "User ID",
+			"type": "integer",
+			"minimum": 1
+		},
+		"certificate_id": {
+			"description": "Certificate ID",
+			"anyOf": [
+				{
+					"type": "integer",
+					"minimum": 0
+				},
+				{
+					"type": "string",
+					"pattern": "^new$"
+				}
+			]
+		},
+		"access_list_id": {
+			"description": "Access List ID",
+			"type": "integer",
+			"minimum": 0
+		},
+		"domain_names": {
+			"description": "Domain Names separated by a comma",
+			"type": "array",
+			"minItems": 1,
+			"maxItems": 100,
+			"uniqueItems": true,
+			"items": {
+				"type": "string",
+				"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
+			}
+		},
+		"enabled": {
+			"description": "Is Enabled",
+			"type": "boolean"
+		},
+		"ssl_forced": {
+			"description": "Is SSL Forced",
+			"type": "boolean"
+		},
+		"hsts_enabled": {
+			"description": "Is HSTS Enabled",
+			"type": "boolean"
+		},
+		"hsts_subdomains": {
+			"description": "Is HSTS applicable to all subdomains",
+			"type": "boolean"
+		},
+		"ssl_provider": {
+			"type": "string",
+			"pattern": "^(letsencrypt|other)$"
+		},
+		"http2_support": {
+			"description": "HTTP2 Protocol Support",
+			"type": "boolean"
+		},
+		"block_exploits": {
+			"description": "Should we block common exploits",
+			"type": "boolean"
+		},
+		"caching_enabled": {
+			"description": "Should we cache assets",
+			"type": "boolean"
+		}
+	}
+}
diff --git a/backend/schema/components/access-list-object.json b/backend/schema/components/access-list-object.json
new file mode 100644
index 000000000..cd0218d72
--- /dev/null
+++ b/backend/schema/components/access-list-object.json
@@ -0,0 +1,53 @@
+{
+	"type": "object",
+	"description": "Access List object",
+	"required": ["id", "created_on", "modified_on", "owner_user_id", "name", "directive", "address", "satisfy_any", "pass_auth", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"name": {
+			"type": "string",
+			"minLength": 1
+		},
+		"directive": {
+			"type": "string",
+			"enum": ["allow", "deny"]
+		},
+		"address": {
+			"oneOf": [
+				{
+					"type": "string",
+					"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+				},
+				{
+					"type": "string",
+					"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+				},
+				{
+					"type": "string",
+					"pattern": "^all$"
+				}
+			]
+		},
+		"satisfy_any": {
+			"type": "boolean"
+		},
+		"pass_auth": {
+			"type": "boolean"
+		},
+		"meta": {
+			"type": "object"
+		}
+	}
+}
diff --git a/backend/schema/components/audit-log-object.json b/backend/schema/components/audit-log-object.json
new file mode 100644
index 000000000..3e5e8594b
--- /dev/null
+++ b/backend/schema/components/audit-log-object.json
@@ -0,0 +1,32 @@
+{
+	"type": "object",
+	"description": "Audit Log object",
+	"required": ["id", "created_on", "modified_on", "user_id", "object_type", "object_id", "action", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"object_type": {
+			"type": "string"
+		},
+		"object_id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"action": {
+			"type": "string"
+		},
+		"meta": {
+			"type": "object"
+		}
+	}
+}
diff --git a/backend/schema/components/certificate-list.json b/backend/schema/components/certificate-list.json
new file mode 100644
index 000000000..cec4db82c
--- /dev/null
+++ b/backend/schema/components/certificate-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "Certificates list",
+	"items": {
+		"$ref": "./certificate-object.json"
+	}
+}
diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json
new file mode 100644
index 000000000..b75dcf61d
--- /dev/null
+++ b/backend/schema/components/certificate-object.json
@@ -0,0 +1,81 @@
+{
+	"type": "object",
+	"description": "Certificate object",
+	"required": ["id", "created_on", "modified_on", "owner_user_id", "provider", "nice_name", "domain_names", "expires_on", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"provider": {
+			"$ref": "../common.json#/properties/ssl_provider"
+		},
+		"nice_name": {
+			"type": "string",
+			"description": "Nice Name for the custom certificate"
+		},
+		"domain_names": {
+			"description": "Domain Names separated by a comma",
+			"type": "array",
+			"maxItems": 100,
+			"uniqueItems": true,
+			"items": {
+				"type": "string",
+				"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
+			}
+		},
+		"expires_on": {
+			"description": "Date and time of expiration",
+			"readOnly": true,
+			"type": "string"
+		},
+		"owner": {
+			"$ref": "./user-object.json"
+		},
+		"meta": {
+			"type": "object",
+			"additionalProperties": false,
+			"properties": {
+				"certificate": {
+					"type": "string",
+					"minLength": 1
+				},
+				"certificate_key": {
+					"type": "string",
+					"minLength": 1
+				},
+				"dns_challenge": {
+					"type": "boolean"
+				},
+				"dns_provider": {
+					"type": "string"
+				},
+				"dns_provider_credentials": {
+					"type": "string"
+				},
+				"letsencrypt_agree": {
+					"type": "boolean"
+				},
+				"letsencrypt_certificate": {
+					"type": "object"
+				},
+				"letsencrypt_email": {
+					"type": "string"
+				},
+				"propagation_seconds": {
+					"type": "integer",
+					"minimum": 0
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/components/dead-host-list.json b/backend/schema/components/dead-host-list.json
new file mode 100644
index 000000000..56ff303ba
--- /dev/null
+++ b/backend/schema/components/dead-host-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "404 Hosts list",
+	"items": {
+		"$ref": "./dead-host-object.json"
+	}
+}
diff --git a/backend/schema/components/dead-host-object.json b/backend/schema/components/dead-host-object.json
new file mode 100644
index 000000000..792c2f81e
--- /dev/null
+++ b/backend/schema/components/dead-host-object.json
@@ -0,0 +1,47 @@
+{
+	"type": "object",
+	"description": "404 Host object",
+	"required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "advanced_config", "enabled", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"domain_names": {
+			"$ref": "../common.json#/properties/domain_names"
+		},
+		"certificate_id": {
+			"$ref": "../common.json#/properties/certificate_id"
+		},
+		"ssl_forced": {
+			"$ref": "../common.json#/properties/ssl_forced"
+		},
+		"hsts_enabled": {
+			"$ref": "../common.json#/properties/hsts_enabled"
+		},
+		"hsts_subdomains": {
+			"$ref": "../common.json#/properties/hsts_subdomains"
+		},
+		"http2_support": {
+			"$ref": "../common.json#/properties/http2_support"
+		},
+		"advanced_config": {
+			"type": "string"
+		},
+		"enabled": {
+			"$ref": "../common.json#/properties/enabled"
+		},
+		"meta": {
+			"type": "object"
+		}
+	}
+}
diff --git a/backend/schema/components/error-object.json b/backend/schema/components/error-object.json
new file mode 100644
index 000000000..c2540cf1f
--- /dev/null
+++ b/backend/schema/components/error-object.json
@@ -0,0 +1,14 @@
+{
+	"type": "object",
+	"description": "Error object",
+	"additionalProperties": false,
+	"required": ["code", "message"],
+	"properties": {
+		"code": {
+			"type": "integer"
+		},
+		"message": {
+			"type": "string"
+		}
+	}
+}
diff --git a/backend/schema/components/error.json b/backend/schema/components/error.json
new file mode 100644
index 000000000..ceb3e1492
--- /dev/null
+++ b/backend/schema/components/error.json
@@ -0,0 +1,9 @@
+{
+	"type": "object",
+	"description": "Error",
+	"properties": {
+		"error": {
+			"$ref": "./error-object.json"
+		}
+	}
+}
diff --git a/backend/schema/components/health-object.json b/backend/schema/components/health-object.json
new file mode 100644
index 000000000..8d223417b
--- /dev/null
+++ b/backend/schema/components/health-object.json
@@ -0,0 +1,38 @@
+{
+	"type": "object",
+	"description": "Health object",
+	"additionalProperties": false,
+	"required": ["status", "version"],
+	"properties": {
+		"status": {
+			"type": "string",
+			"description": "Healthy",
+			"example": "OK"
+		},
+		"version": {
+			"type": "object",
+			"description": "The version object",
+			"example": {
+				"major": 2,
+				"minor": 0,
+				"revision": 0
+			},
+			"additionalProperties": false,
+			"required": ["major", "minor", "revision"],
+			"properties": {
+				"major": {
+					"type": "integer",
+					"minimum": 0
+				},
+				"minor": {
+					"type": "integer",
+					"minimum": 0
+				},
+				"revision": {
+					"type": "integer",
+					"minimum": 0
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/components/permission-object.json b/backend/schema/components/permission-object.json
new file mode 100644
index 000000000..b852a014d
--- /dev/null
+++ b/backend/schema/components/permission-object.json
@@ -0,0 +1,41 @@
+{
+	"type": "object",
+	"minProperties": 1,
+	"properties": {
+		"visibility": {
+			"type": "string",
+			"description": "Visibility Type",
+			"enum": ["all", "user"]
+		},
+		"access_lists": {
+			"type": "string",
+			"description": "Access Lists Permissions",
+			"enum": ["hidden", "view", "manage"]
+		},
+		"dead_hosts": {
+			"type": "string",
+			"description": "404 Hosts Permissions",
+			"enum": ["hidden", "view", "manage"]
+		},
+		"proxy_hosts": {
+			"type": "string",
+			"description": "Proxy Hosts Permissions",
+			"enum": ["hidden", "view", "manage"]
+		},
+		"redirection_hosts": {
+			"type": "string",
+			"description": "Redirection Permissions",
+			"enum": ["hidden", "view", "manage"]
+		},
+		"streams": {
+			"type": "string",
+			"description": "Streams Permissions",
+			"enum": ["hidden", "view", "manage"]
+		},
+		"certificates": {
+			"type": "string",
+			"description": "Certificates Permissions",
+			"enum": ["hidden", "view", "manage"]
+		}
+	}
+}
diff --git a/backend/schema/components/proxy-host-list.json b/backend/schema/components/proxy-host-list.json
new file mode 100644
index 000000000..39789b4a7
--- /dev/null
+++ b/backend/schema/components/proxy-host-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "Proxy Hosts list",
+	"items": {
+		"$ref": "./proxy-host-object.json"
+	}
+}
diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json
new file mode 100644
index 000000000..e9dcacb5e
--- /dev/null
+++ b/backend/schema/components/proxy-host-object.json
@@ -0,0 +1,153 @@
+{
+	"type": "object",
+	"description": "Proxy Host object",
+	"required": [
+		"id",
+		"created_on",
+		"modified_on",
+		"owner_user_id",
+		"domain_names",
+		"forward_host",
+		"forward_port",
+		"access_list_id",
+		"certificate_id",
+		"ssl_forced",
+		"caching_enabled",
+		"block_exploits",
+		"advanced_config",
+		"meta",
+		"allow_websocket_upgrade",
+		"http2_support",
+		"forward_scheme",
+		"enabled",
+		"locations",
+		"hsts_enabled",
+		"hsts_subdomains"
+	],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"domain_names": {
+			"$ref": "../common.json#/properties/domain_names"
+		},
+		"forward_host": {
+			"type": "string",
+			"minLength": 1,
+			"maxLength": 255
+		},
+		"forward_port": {
+			"type": "integer",
+			"minimum": 1,
+			"maximum": 65535
+		},
+		"access_list_id": {
+			"$ref": "../common.json#/properties/access_list_id"
+		},
+		"certificate_id": {
+			"$ref": "../common.json#/properties/certificate_id"
+		},
+		"ssl_forced": {
+			"$ref": "../common.json#/properties/ssl_forced"
+		},
+		"caching_enabled": {
+			"$ref": "../common.json#/properties/caching_enabled"
+		},
+		"block_exploits": {
+			"$ref": "../common.json#/properties/block_exploits"
+		},
+		"advanced_config": {
+			"type": "string"
+		},
+		"meta": {
+			"type": "object"
+		},
+		"allow_websocket_upgrade": {
+			"description": "Allow Websocket Upgrade for all paths",
+			"example": true,
+			"type": "boolean"
+		},
+		"http2_support": {
+			"$ref": "../common.json#/properties/http2_support"
+		},
+		"forward_scheme": {
+			"type": "string",
+			"enum": ["http", "https"]
+		},
+		"enabled": {
+			"$ref": "../common.json#/properties/enabled"
+		},
+		"locations": {
+			"type": "array",
+			"minItems": 0,
+			"items": {
+				"type": "object",
+				"required": ["forward_scheme", "forward_host", "forward_port", "path"],
+				"additionalProperties": false,
+				"properties": {
+					"id": {
+						"type": ["integer", "null"]
+					},
+					"path": {
+						"type": "string",
+						"minLength": 1
+					},
+					"forward_scheme": {
+						"$ref": "#/properties/forward_scheme"
+					},
+					"forward_host": {
+						"$ref": "#/properties/forward_host"
+					},
+					"forward_port": {
+						"$ref": "#/properties/forward_port"
+					},
+					"forward_path": {
+						"type": "string"
+					},
+					"advanced_config": {
+						"type": "string"
+					}
+				}
+			}
+		},
+		"hsts_enabled": {
+			"$ref": "../common.json#/properties/hsts_enabled"
+		},
+		"hsts_subdomains": {
+			"$ref": "../common.json#/properties/hsts_subdomains"
+		},
+		"certificate": {
+			"oneOf": [
+				{
+					"type": "null"
+				},
+				{
+					"$ref": "./certificate-object.json"
+				}
+			]
+		},
+		"owner": {
+			"$ref": "./user-object.json"
+		},
+		"access_list": {
+			"oneOf": [
+				{
+					"type": "null"
+				},
+				{
+					"$ref": "./access-list-object.json"
+				}
+			]
+		}
+	}
+}
diff --git a/backend/schema/components/redirection-host-list.json b/backend/schema/components/redirection-host-list.json
new file mode 100644
index 000000000..716dcfa1d
--- /dev/null
+++ b/backend/schema/components/redirection-host-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "Redirection Hosts list",
+	"items": {
+		"$ref": "./redirection-host-object.json"
+	}
+}
diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json
new file mode 100644
index 000000000..e7a495fd3
--- /dev/null
+++ b/backend/schema/components/redirection-host-object.json
@@ -0,0 +1,72 @@
+{
+	"type": "object",
+	"description": "Redirection Host object",
+	"required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "forward_http_code", "forward_scheme", "forward_domain_name", "preserve_path", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "block_exploits", "advanced_config", "enabled", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"domain_names": {
+			"$ref": "../common.json#/properties/domain_names"
+		},
+		"forward_http_code": {
+			"description": "Redirect HTTP Status Code",
+			"example": 302,
+			"type": "integer",
+			"minimum": 300,
+			"maximum": 308
+		},
+		"forward_scheme": {
+			"type": "string",
+			"enum": ["auto", "http", "https"]
+		},
+		"forward_domain_name": {
+			"description": "Domain Name",
+			"example": "jc21.com",
+			"type": "string",
+			"pattern": "^(?:[^.*]+\\.?)+[^.]$"
+		},
+		"preserve_path": {
+			"description": "Should the path be preserved",
+			"example": true,
+			"type": "boolean"
+		},
+		"certificate_id": {
+			"$ref": "../common.json#/properties/certificate_id"
+		},
+		"ssl_forced": {
+			"$ref": "../common.json#/properties/ssl_forced"
+		},
+		"hsts_enabled": {
+			"$ref": "../common.json#/properties/hsts_enabled"
+		},
+		"hsts_subdomains": {
+			"$ref": "../common.json#/properties/hsts_subdomains"
+		},
+		"http2_support": {
+			"$ref": "../common.json#/properties/http2_support"
+		},
+		"block_exploits": {
+			"$ref": "../common.json#/properties/block_exploits"
+		},
+		"advanced_config": {
+			"type": "string"
+		},
+		"enabled": {
+			"$ref": "../common.json#/properties/enabled"
+		},
+		"meta": {
+			"type": "object"
+		}
+	}
+}
diff --git a/backend/schema/components/security-schemes.json b/backend/schema/components/security-schemes.json
new file mode 100644
index 000000000..82407be3f
--- /dev/null
+++ b/backend/schema/components/security-schemes.json
@@ -0,0 +1,6 @@
+{
+	"BearerAuth": {
+		"type": "http",
+		"scheme": "bearer"
+	}
+}
diff --git a/backend/schema/components/setting-list.json b/backend/schema/components/setting-list.json
new file mode 100644
index 000000000..c66f099ea
--- /dev/null
+++ b/backend/schema/components/setting-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "Setting list",
+	"items": {
+		"$ref": "./setting-object.json"
+	}
+}
diff --git a/backend/schema/components/setting-object.json b/backend/schema/components/setting-object.json
new file mode 100644
index 000000000..b9c6a1039
--- /dev/null
+++ b/backend/schema/components/setting-object.json
@@ -0,0 +1,56 @@
+{
+	"type": "object",
+	"description": "Setting object",
+	"required": ["id", "name", "description", "value", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"type": "string",
+			"description": "Setting ID",
+			"minLength": 1,
+			"example": "default-site"
+		},
+		"name": {
+			"type": "string",
+			"description": "Setting Display Name",
+			"minLength": 1,
+			"example": "Default Site"
+		},
+		"description": {
+			"type": "string",
+			"description": "Meaningful description",
+			"minLength": 1,
+			"example": "What to show when Nginx is hit with an unknown Host"
+		},
+		"value": {
+			"description": "Value in almost any form",
+			"example": "congratulations",
+			"anyOf": [
+				{
+					"type": "string",
+					"minLength": 1
+				},
+				{
+					"type": "integer"
+				},
+				{
+					"type": "object"
+				},
+				{
+					"type": "number"
+				},
+				{
+					"type": "array"
+				}
+			]
+		},
+		"meta": {
+			"description": "Extra metadata",
+			"example": {
+				"redirect": "http://example.com",
+				"html": "<h1>404</h1>"
+			},
+			"type": "object"
+		}
+	}
+}
diff --git a/backend/schema/components/stream-list.json b/backend/schema/components/stream-list.json
new file mode 100644
index 000000000..39789b4a7
--- /dev/null
+++ b/backend/schema/components/stream-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "Proxy Hosts list",
+	"items": {
+		"$ref": "./proxy-host-object.json"
+	}
+}
diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json
new file mode 100644
index 000000000..848c30e6e
--- /dev/null
+++ b/backend/schema/components/stream-object.json
@@ -0,0 +1,76 @@
+{
+	"type": "object",
+	"description": "Stream object",
+	"required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"$ref": "../common.json#/properties/id"
+		},
+		"created_on": {
+			"$ref": "../common.json#/properties/created_on"
+		},
+		"modified_on": {
+			"$ref": "../common.json#/properties/modified_on"
+		},
+		"owner_user_id": {
+			"$ref": "../common.json#/properties/user_id"
+		},
+		"incoming_port": {
+			"type": "integer",
+			"minimum": 1,
+			"maximum": 65535
+		},
+		"forwarding_host": {
+			"anyOf": [
+				{
+					"description": "Domain Name",
+					"example": "jc21.com",
+					"type": "string",
+					"pattern": "^(?:[^.*]+\\.?)+[^.]$"
+				},
+				{
+					"type": "string",
+					"format": "ipv4"
+				},
+				{
+					"type": "string",
+					"format": "ipv6"
+				}
+			]
+		},
+		"forwarding_port": {
+			"type": "integer",
+			"minimum": 1,
+			"maximum": 65535
+		},
+		"tcp_forwarding": {
+			"type": "boolean"
+		},
+		"udp_forwarding": {
+			"type": "boolean"
+		},
+		"enabled": {
+			"$ref": "../common.json#/properties/enabled"
+		},
+		"certificate_id": {
+			"$ref": "../common.json#/properties/certificate_id"
+		},
+		"meta": {
+			"type": "object"
+		},
+		"owner": {
+			"$ref": "./user-object.json"
+		},
+		"certificate": {
+			"oneOf": [
+				{
+					"type": "null"
+				},
+				{
+					"$ref": "./certificate-object.json"
+				}
+			]
+		}
+	}
+}
diff --git a/backend/schema/components/token-object.json b/backend/schema/components/token-object.json
new file mode 100644
index 000000000..6ec4e4348
--- /dev/null
+++ b/backend/schema/components/token-object.json
@@ -0,0 +1,18 @@
+{
+	"type": "object",
+	"description": "Token object",
+	"required": ["expires", "token"],
+	"additionalProperties": false,
+	"properties": {
+		"expires": {
+			"description": "Token Expiry ISO Time String",
+			"example": "2025-02-04T20:40:46.340Z",
+			"type": "string"
+		},
+		"token": {
+			"description": "JWT Token",
+			"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
+			"type": "string"
+		}
+	}
+}
diff --git a/backend/schema/components/user-list.json b/backend/schema/components/user-list.json
new file mode 100644
index 000000000..c5c0f7116
--- /dev/null
+++ b/backend/schema/components/user-list.json
@@ -0,0 +1,7 @@
+{
+	"type": "array",
+	"description": "User list",
+	"items": {
+		"$ref": "./user-object.json"
+	}
+}
diff --git a/backend/schema/components/user-object.json b/backend/schema/components/user-object.json
new file mode 100644
index 000000000..180e8f197
--- /dev/null
+++ b/backend/schema/components/user-object.json
@@ -0,0 +1,59 @@
+{
+	"type": "object",
+	"description": "User object",
+	"required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"],
+	"additionalProperties": false,
+	"properties": {
+		"id": {
+			"type": "integer",
+			"description": "User ID",
+			"minimum": 1,
+			"example": 1
+		},
+		"created_on": {
+			"type": "string",
+			"description": "Created Date",
+			"example": "2020-01-30T09:36:08.000Z"
+		},
+		"modified_on": {
+			"type": "string",
+			"description": "Modified Date",
+			"example": "2020-01-30T09:41:04.000Z"
+		},
+		"is_disabled": {
+			"type": "boolean",
+			"description": "Is user Disabled",
+			"example": true
+		},
+		"email": {
+			"type": "string",
+			"description": "Email",
+			"minLength": 3,
+			"example": "jc@jc21.com"
+		},
+		"name": {
+			"type": "string",
+			"description": "Name",
+			"minLength": 1,
+			"example": "Jamie Curnow"
+		},
+		"nickname": {
+			"type": "string",
+			"description": "Nickname",
+			"example": "James"
+		},
+		"avatar": {
+			"type": "string",
+			"description": "Gravatar URL based on email, without scheme",
+			"example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm"
+		},
+		"roles": {
+			"description": "Roles applied",
+			"example": ["admin"],
+			"type": "array",
+			"items": {
+				"type": "string"
+			}
+		}
+	}
+}
diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json
deleted file mode 100644
index 640093a08..000000000
--- a/backend/schema/definitions.json
+++ /dev/null
@@ -1,240 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "definitions",
-  "definitions": {
-    "id": {
-      "description": "Unique identifier",
-      "example": 123456,
-      "readOnly": true,
-      "type": "integer",
-      "minimum": 1
-    },
-    "setting_id": {
-      "description": "Unique identifier for a Setting",
-      "example": "default-site",
-      "readOnly": true,
-      "type": "string",
-      "minLength": 2
-    },
-    "token": {
-      "type": "string",
-      "minLength": 10
-    },
-    "expand": {
-      "anyOf": [
-        {
-          "type": "null"
-        },
-        {
-          "type": "array",
-          "minItems": 1,
-          "items": {
-            "type": "string"
-          }
-        }
-      ]
-    },
-    "sort": {
-      "type": "array",
-      "minItems": 1,
-      "items": {
-        "type": "object",
-        "required": [
-          "field",
-          "dir"
-        ],
-        "additionalProperties": false,
-        "properties": {
-          "field": {
-            "type": "string"
-          },
-          "dir": {
-            "type": "string",
-            "pattern": "^(asc|desc)$"
-          }
-        }
-      }
-    },
-    "query": {
-      "anyOf": [
-        {
-          "type": "null"
-        },
-        {
-          "type": "string",
-          "minLength": 1,
-          "maxLength": 255
-        }
-      ]
-    },
-    "criteria": {
-      "anyOf": [
-        {
-          "type": "null"
-        },
-        {
-          "type": "object"
-        }
-      ]
-    },
-    "fields": {
-      "anyOf": [
-        {
-          "type": "null"
-        },
-        {
-          "type": "array",
-          "minItems": 1,
-          "items": {
-            "type": "string"
-          }
-        }
-      ]
-    },
-    "omit": {
-      "anyOf": [
-        {
-          "type": "null"
-        },
-        {
-          "type": "array",
-          "minItems": 1,
-          "items": {
-            "type": "string"
-          }
-        }
-      ]
-    },
-    "created_on": {
-      "description": "Date and time of creation",
-      "format": "date-time",
-      "readOnly": true,
-      "type": "string"
-    },
-    "modified_on": {
-      "description": "Date and time of last update",
-      "format": "date-time",
-      "readOnly": true,
-      "type": "string"
-    },
-    "user_id": {
-      "description": "User ID",
-      "example": 1234,
-      "type": "integer",
-      "minimum": 1
-    },
-    "certificate_id": {
-      "description": "Certificate ID",
-      "example": 1234,
-      "anyOf": [
-        {
-          "type": "integer",
-          "minimum": 0
-        },
-        {
-          "type": "string",
-          "pattern": "^new$"
-        }
-      ]
-    },
-    "access_list_id": {
-      "description": "Access List ID",
-      "example": 1234,
-      "type": "integer",
-      "minimum": 0
-    },
-    "name": {
-      "type": "string",
-      "minLength": 1,
-      "maxLength": 255
-    },
-    "email": {
-      "description": "Email Address",
-      "example": "john@example.com",
-      "format": "email",
-      "type": "string",
-      "minLength": 6,
-      "maxLength": 100
-    },
-    "password": {
-      "description": "Password",
-      "type": "string",
-      "minLength": 8,
-      "maxLength": 255
-    },
-    "domain_name": {
-      "description": "Domain Name",
-      "example": "jc21.com",
-      "type": "string",
-      "pattern": "^(?:[^.*]+\\.?)+[^.]$"
-    },
-    "domain_names": {
-      "description": "Domain Names separated by a comma",
-      "example": "*.jc21.com,blog.jc21.com",
-      "type": "array",
-      "maxItems": 100,
-      "uniqueItems": true,
-      "items": {
-        "type": "string",
-        "pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$"
-      }
-    },
-    "http_code": {
-      "description": "Redirect HTTP Status Code",
-      "example": 302,
-      "type": "integer",
-      "minimum": 300,
-      "maximum": 308
-    },
-    "scheme": {
-      "description": "RFC Protocol",
-      "example": "HTTPS or $scheme",
-      "type": "string",
-      "minLength": 4
-    },
-    "enabled": {
-      "description": "Is Enabled",
-      "example": true,
-      "type": "boolean"
-    },
-    "ssl_enabled": {
-      "description": "Is SSL Enabled",
-      "example": true,
-      "type": "boolean"
-    },
-    "ssl_forced": {
-      "description": "Is SSL Forced",
-      "example": false,
-      "type": "boolean"
-    },
-    "hsts_enabled": {
-      "description": "Is HSTS Enabled",
-      "example": false,
-      "type": "boolean"
-    },
-    "hsts_subdomains": {
-      "description": "Is HSTS applicable to all subdomains",
-      "example": false,
-      "type": "boolean"
-    },
-    "ssl_provider": {
-      "type": "string",
-      "pattern": "^(letsencrypt|other)$"
-    },
-    "http2_support": {
-      "description": "HTTP2 Protocol Support",
-      "example": false,
-      "type": "boolean"
-    },
-    "block_exploits": {
-      "description": "Should we block common exploits",
-      "example": true,
-      "type": "boolean"
-    },
-    "caching_enabled": {
-      "description": "Should we cache assets",
-      "example": true,
-      "type": "boolean"
-    }
-  }
-}
diff --git a/backend/schema/endpoints/access-lists.json b/backend/schema/endpoints/access-lists.json
deleted file mode 100644
index 404e32376..000000000
--- a/backend/schema/endpoints/access-lists.json
+++ /dev/null
@@ -1,236 +0,0 @@
-{
-	"$schema": "http://json-schema.org/draft-07/schema#",
-	"$id": "endpoints/access-lists",
-	"title": "Access Lists",
-	"description": "Endpoints relating to Access Lists",
-	"stability": "stable",
-	"type": "object",
-	"definitions": {
-		"id": {
-			"$ref": "../definitions.json#/definitions/id"
-		},
-		"created_on": {
-			"$ref": "../definitions.json#/definitions/created_on"
-		},
-		"modified_on": {
-			"$ref": "../definitions.json#/definitions/modified_on"
-		},
-		"name": {
-			"type": "string",
-			"description": "Name of the Access List"
-		},
-		"directive": {
-			"type": "string",
-			"enum": ["allow", "deny"]
-		},
-		"address": {
-			"oneOf": [
-				{
-					"type": "string",
-					"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
-				},
-				{
-					"type": "string",
-					"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
-				},
-				{
-					"type": "string",
-					"pattern": "^all$"
-				}
-			]
-		},
-		"satisfy_any": {
-			"type": "boolean"
-		},
-		"pass_auth": {
-			"type": "boolean"
-		},
-		"meta": {
-			"type": "object"
-		}
-	},
-	"properties": {
-		"id": {
-			"$ref": "#/definitions/id"
-		},
-		"created_on": {
-			"$ref": "#/definitions/created_on"
-		},
-		"modified_on": {
-			"$ref": "#/definitions/modified_on"
-		},
-		"name": {
-			"$ref": "#/definitions/name"
-		},
-		"meta": {
-			"$ref": "#/definitions/meta"
-		}
-	},
-	"links": [
-		{
-			"title": "List",
-			"description": "Returns a list of Access Lists",
-			"href": "/nginx/access-lists",
-			"access": "private",
-			"method": "GET",
-			"rel": "self",
-			"http_header": {
-				"$ref": "../examples.json#/definitions/auth_header"
-			},
-			"targetSchema": {
-				"type": "array",
-				"items": {
-					"$ref": "#/properties"
-				}
-			}
-		},
-		{
-			"title": "Create",
-			"description": "Creates a new Access List",
-			"href": "/nginx/access-list",
-			"access": "private",
-			"method": "POST",
-			"rel": "create",
-			"http_header": {
-				"$ref": "../examples.json#/definitions/auth_header"
-			},
-			"schema": {
-				"type": "object",
-				"additionalProperties": false,
-				"required": ["name"],
-				"properties": {
-					"name": {
-						"$ref": "#/definitions/name"
-					},
-					"satisfy_any": {
-						"$ref": "#/definitions/satisfy_any"
-					},
-					"pass_auth": {
-						"$ref": "#/definitions/pass_auth"
-					},
-					"items": {
-						"type": "array",
-						"minItems": 0,
-						"items": {
-							"type": "object",
-							"additionalProperties": false,
-							"properties": {
-								"username": {
-									"type": "string",
-									"minLength": 1
-								},
-								"password": {
-									"type": "string",
-									"minLength": 1
-								}
-							}
-						}
-					},
-					"clients": {
-						"type": "array",
-						"minItems": 0,
-						"items": {
-							"type": "object",
-							"additionalProperties": false,
-							"properties": {
-								"address": {
-									"$ref": "#/definitions/address"
-								},
-								"directive": {
-									"$ref": "#/definitions/directive"
-								}
-							}
-						}
-					},
-					"meta": {
-						"$ref": "#/definitions/meta"
-					}
-				}
-			},
-			"targetSchema": {
-				"properties": {
-					"$ref": "#/properties"
-				}
-			}
-		},
-		{
-			"title": "Update",
-			"description": "Updates a existing Access List",
-			"href": "/nginx/access-list/{definitions.identity.example}",
-			"access": "private",
-			"method": "PUT",
-			"rel": "update",
-			"http_header": {
-				"$ref": "../examples.json#/definitions/auth_header"
-			},
-			"schema": {
-				"type": "object",
-				"additionalProperties": false,
-				"properties": {
-					"name": {
-						"$ref": "#/definitions/name"
-					},
-					"satisfy_any": {
-						"$ref": "#/definitions/satisfy_any"
-					},
-					"pass_auth": {
-						"$ref": "#/definitions/pass_auth"
-					},
-					"items": {
-						"type": "array",
-						"minItems": 0,
-						"items": {
-							"type": "object",
-							"additionalProperties": false,
-							"properties": {
-								"username": {
-									"type": "string",
-									"minLength": 1
-								},
-								"password": {
-									"type": "string",
-									"minLength": 0
-								}
-							}
-						}
-					},
-					"clients": {
-						"type": "array",
-						"minItems": 0,
-						"items": {
-							"type": "object",
-							"additionalProperties": false,
-							"properties": {
-								"address": {
-									"$ref": "#/definitions/address"
-								},
-								"directive": {
-									"$ref": "#/definitions/directive"
-								}
-							}
-						}
-					}
-				}
-			},
-			"targetSchema": {
-				"properties": {
-					"$ref": "#/properties"
-				}
-			}
-		},
-		{
-			"title": "Delete",
-			"description": "Deletes a existing Access List",
-			"href": "/nginx/access-list/{definitions.identity.example}",
-			"access": "private",
-			"method": "DELETE",
-			"rel": "delete",
-			"http_header": {
-				"$ref": "../examples.json#/definitions/auth_header"
-			},
-			"targetSchema": {
-				"type": "boolean"
-			}
-		}
-	]
-}
diff --git a/backend/schema/endpoints/certificates.json b/backend/schema/endpoints/certificates.json
deleted file mode 100644
index 955ca75c9..000000000
--- a/backend/schema/endpoints/certificates.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/certificates",
-  "title": "Certificates",
-  "description": "Endpoints relating to Certificates",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "provider": {
-      "$ref": "../definitions.json#/definitions/ssl_provider"
-    },
-    "nice_name": {
-      "type": "string",
-      "description": "Nice Name for the custom certificate"
-    },
-    "domain_names": {
-      "$ref": "../definitions.json#/definitions/domain_names"
-    },
-    "expires_on": {
-      "description": "Date and time of expiration",
-      "format": "date-time",
-      "readOnly": true,
-      "type": "string"
-    },
-    "meta": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "letsencrypt_email": {
-          "type": "string",
-          "format": "email"
-        },
-        "letsencrypt_agree": {
-          "type": "boolean"
-        },
-        "dns_challenge": {
-          "type": "boolean"
-        },
-        "dns_provider": {
-          "type": "string"
-        },
-        "dns_provider_credentials": {
-          "type": "string"
-        },
-        "propagation_seconds": {
-          "anyOf": [
-            { 
-              "type": "integer",
-              "minimum": 0 
-            }
-          ]
-          
-        }
-      }
-    }
-  },
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "provider": {
-      "$ref": "#/definitions/provider"
-    },
-    "nice_name": {
-      "$ref": "#/definitions/nice_name"
-    },
-    "domain_names": {
-      "$ref": "#/definitions/domain_names"
-    },
-    "expires_on": {
-      "$ref": "#/definitions/expires_on"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Certificates",
-      "href": "/nginx/certificates",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new Certificate",
-      "href": "/nginx/certificates",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "provider"
-        ],
-        "properties": {
-          "provider": {
-            "$ref": "#/definitions/provider"
-          },
-          "nice_name": {
-            "$ref": "#/definitions/nice_name"
-          },
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing Certificate",
-      "href": "/nginx/certificates/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Test HTTP Challenge",
-      "description": "Tests whether the HTTP challenge should work",
-      "href": "/nginx/certificates/{definitions.identity.example}/test-http",
-      "access": "private",
-      "method": "GET",
-      "rel": "info",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/dead-hosts.json b/backend/schema/endpoints/dead-hosts.json
deleted file mode 100644
index 0c73c3be1..000000000
--- a/backend/schema/endpoints/dead-hosts.json
+++ /dev/null
@@ -1,240 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/dead-hosts",
-  "title": "404 Hosts",
-  "description": "Endpoints relating to 404 Hosts",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "../definitions.json#/definitions/domain_names"
-    },
-    "certificate_id": {
-      "$ref": "../definitions.json#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "../definitions.json#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "../definitions.json#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "../definitions.json#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "../definitions.json#/definitions/http2_support"
-    },
-    "advanced_config": {
-      "type": "string"
-    },
-    "enabled": {
-      "$ref": "../definitions.json#/definitions/enabled"
-    },
-    "meta": {
-      "type": "object"
-    }
-  },
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "#/definitions/domain_names"
-    },
-    "certificate_id": {
-      "$ref": "#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "#/definitions/http2_support"
-    },
-    "advanced_config": {
-      "$ref": "#/definitions/advanced_config"
-    },
-    "enabled": {
-      "$ref": "#/definitions/enabled"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of 404 Hosts",
-      "href": "/nginx/dead-hosts",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new 404 Host",
-      "href": "/nginx/dead-hosts",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "domain_names"
-        ],
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing 404 Host",
-      "href": "/nginx/dead-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing 404 Host",
-      "href": "/nginx/dead-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Enable",
-      "description": "Enables a existing 404 Host",
-      "href": "/nginx/dead-hosts/{definitions.identity.example}/enable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Disable",
-      "description": "Disables a existing 404 Host",
-      "href": "/nginx/dead-hosts/{definitions.identity.example}/disable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json
deleted file mode 100644
index 9a3fff2fc..000000000
--- a/backend/schema/endpoints/proxy-hosts.json
+++ /dev/null
@@ -1,387 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/proxy-hosts",
-  "title": "Proxy Hosts",
-  "description": "Endpoints relating to Proxy Hosts",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "../definitions.json#/definitions/domain_names"
-    },
-    "forward_scheme": {
-      "type": "string",
-      "enum": ["http", "https"]
-    },
-    "forward_host": {
-      "type": "string",
-      "minLength": 1,
-      "maxLength": 255
-    },
-    "forward_port": {
-      "type": "integer",
-      "minimum": 1,
-      "maximum": 65535
-    },
-    "certificate_id": {
-      "$ref": "../definitions.json#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "../definitions.json#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "../definitions.json#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "../definitions.json#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "../definitions.json#/definitions/http2_support"
-    },
-    "block_exploits": {
-      "$ref": "../definitions.json#/definitions/block_exploits"
-    },
-    "caching_enabled": {
-      "$ref": "../definitions.json#/definitions/caching_enabled"
-    },
-    "allow_websocket_upgrade": {
-      "description": "Allow Websocket Upgrade for all paths",
-      "example": true,
-      "type": "boolean"
-    },
-    "access_list_id": {
-      "$ref": "../definitions.json#/definitions/access_list_id"
-    },
-    "advanced_config": {
-      "type": "string"
-    },
-    "enabled": {
-      "$ref": "../definitions.json#/definitions/enabled"
-    },
-    "meta": {
-      "type": "object"
-    },
-    "locations": {
-      "type": "array",
-      "minItems": 0,
-      "items": {
-        "type": "object",
-        "required": [
-          "forward_scheme",
-          "forward_host",
-          "forward_port",
-          "path"
-        ],
-        "additionalProperties": false,
-        "properties": {
-          "id": {
-            "type": ["integer", "null"]
-          },
-          "path": {
-            "type": "string",
-            "minLength": 1
-          },
-          "forward_scheme": {
-            "$ref": "#/definitions/forward_scheme"
-          },
-          "forward_host": {
-            "$ref": "#/definitions/forward_host"
-          },
-          "forward_port": {
-            "$ref": "#/definitions/forward_port"
-          },
-          "forward_path": {
-            "type": "string"
-          },
-          "advanced_config": {
-            "type": "string"
-          }
-        }
-      }
-    }
-  },
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "#/definitions/domain_names"
-    },
-    "forward_scheme": {
-      "$ref": "#/definitions/forward_scheme"
-    },
-    "forward_host": {
-      "$ref": "#/definitions/forward_host"
-    },
-    "forward_port": {
-      "$ref": "#/definitions/forward_port"
-    },
-    "certificate_id": {
-      "$ref": "#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "#/definitions/http2_support"
-    },
-    "block_exploits": {
-      "$ref": "#/definitions/block_exploits"
-    },
-    "caching_enabled": {
-      "$ref": "#/definitions/caching_enabled"
-    },
-    "allow_websocket_upgrade": {
-      "$ref": "#/definitions/allow_websocket_upgrade"
-    },
-    "access_list_id": {
-      "$ref": "#/definitions/access_list_id"
-    },
-    "advanced_config": {
-      "$ref": "#/definitions/advanced_config"
-    },
-    "enabled": {
-      "$ref": "#/definitions/enabled"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    },
-    "locations": {
-      "$ref": "#/definitions/locations"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Proxy Hosts",
-      "href": "/nginx/proxy-hosts",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new Proxy Host",
-      "href": "/nginx/proxy-hosts",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "domain_names",
-          "forward_scheme",
-          "forward_host",
-          "forward_port"
-        ],
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "forward_scheme": {
-            "$ref": "#/definitions/forward_scheme"
-          },
-          "forward_host": {
-            "$ref": "#/definitions/forward_host"
-          },
-          "forward_port": {
-            "$ref": "#/definitions/forward_port"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "block_exploits": {
-            "$ref": "#/definitions/block_exploits"
-          },
-          "caching_enabled": {
-            "$ref": "#/definitions/caching_enabled"
-          },
-          "allow_websocket_upgrade": {
-            "$ref": "#/definitions/allow_websocket_upgrade"
-          },
-          "access_list_id": {
-            "$ref": "#/definitions/access_list_id"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "enabled": {
-            "$ref": "#/definitions/enabled"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          },
-          "locations": {
-            "$ref": "#/definitions/locations"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing Proxy Host",
-      "href": "/nginx/proxy-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "forward_scheme": {
-            "$ref": "#/definitions/forward_scheme"
-          },
-          "forward_host": {
-            "$ref": "#/definitions/forward_host"
-          },
-          "forward_port": {
-            "$ref": "#/definitions/forward_port"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "block_exploits": {
-            "$ref": "#/definitions/block_exploits"
-          },
-          "caching_enabled": {
-            "$ref": "#/definitions/caching_enabled"
-          },
-          "allow_websocket_upgrade": {
-            "$ref": "#/definitions/allow_websocket_upgrade"
-          },
-          "access_list_id": {
-            "$ref": "#/definitions/access_list_id"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "enabled": {
-            "$ref": "#/definitions/enabled"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          },
-          "locations": {
-            "$ref": "#/definitions/locations"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing Proxy Host",
-      "href": "/nginx/proxy-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Enable",
-      "description": "Enables a existing Proxy Host",
-      "href": "/nginx/proxy-hosts/{definitions.identity.example}/enable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Disable",
-      "description": "Disables a existing Proxy Host",
-      "href": "/nginx/proxy-hosts/{definitions.identity.example}/disable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/redirection-hosts.json b/backend/schema/endpoints/redirection-hosts.json
deleted file mode 100644
index 14a469985..000000000
--- a/backend/schema/endpoints/redirection-hosts.json
+++ /dev/null
@@ -1,305 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/redirection-hosts",
-  "title": "Redirection Hosts",
-  "description": "Endpoints relating to Redirection Hosts",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "../definitions.json#/definitions/domain_names"
-    },
-    "forward_http_code": {
-      "$ref": "../definitions.json#/definitions/http_code"
-    },
-    "forward_scheme": {
-      "$ref": "../definitions.json#/definitions/scheme"
-    },
-    "forward_domain_name": {
-      "$ref": "../definitions.json#/definitions/domain_name"
-    },
-    "preserve_path": {
-      "description": "Should the path be preserved",
-      "example": true,
-      "type": "boolean"
-    },
-    "certificate_id": {
-      "$ref": "../definitions.json#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "../definitions.json#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "../definitions.json#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "../definitions.json#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "../definitions.json#/definitions/http2_support"
-    },
-    "block_exploits": {
-      "$ref": "../definitions.json#/definitions/block_exploits"
-    },
-    "advanced_config": {
-      "type": "string"
-    },
-    "enabled": {
-      "$ref": "../definitions.json#/definitions/enabled"
-    },
-    "meta": {
-      "type": "object"
-    }
-  },
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "domain_names": {
-      "$ref": "#/definitions/domain_names"
-    },
-    "forward_http_code": {
-      "$ref": "#/definitions/forward_http_code"
-    },
-    "forward_scheme": {
-      "$ref": "#/definitions/forward_scheme"
-    },
-    "forward_domain_name": {
-      "$ref": "#/definitions/forward_domain_name"
-    },
-    "preserve_path": {
-      "$ref": "#/definitions/preserve_path"
-    },
-    "certificate_id": {
-      "$ref": "#/definitions/certificate_id"
-    },
-    "ssl_forced": {
-      "$ref": "#/definitions/ssl_forced"
-    },
-    "hsts_enabled": {
-      "$ref": "#/definitions/hsts_enabled"
-    },
-    "hsts_subdomains": {
-      "$ref": "#/definitions/hsts_subdomains"
-    },
-    "http2_support": {
-      "$ref": "#/definitions/http2_support"
-    },
-    "block_exploits": {
-      "$ref": "#/definitions/block_exploits"
-    },
-    "advanced_config": {
-      "$ref": "#/definitions/advanced_config"
-    },
-    "enabled": {
-      "$ref": "#/definitions/enabled"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Redirection Hosts",
-      "href": "/nginx/redirection-hosts",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new Redirection Host",
-      "href": "/nginx/redirection-hosts",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "domain_names",
-          "forward_scheme",
-          "forward_http_code",
-          "forward_domain_name"
-        ],
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "forward_http_code": {
-            "$ref": "#/definitions/forward_http_code"
-          },
-          "forward_scheme": {
-            "$ref": "#/definitions/forward_scheme"
-          },
-          "forward_domain_name": {
-            "$ref": "#/definitions/forward_domain_name"
-          },
-          "preserve_path": {
-            "$ref": "#/definitions/preserve_path"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "block_exploits": {
-            "$ref": "#/definitions/block_exploits"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing Redirection Host",
-      "href": "/nginx/redirection-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "domain_names": {
-            "$ref": "#/definitions/domain_names"
-          },
-          "forward_http_code": {
-            "$ref": "#/definitions/forward_http_code"
-          },
-          "forward_scheme": {
-            "$ref": "#/definitions/forward_scheme"
-          },
-          "forward_domain_name": {
-            "$ref": "#/definitions/forward_domain_name"
-          },
-          "preserve_path": {
-            "$ref": "#/definitions/preserve_path"
-          },
-          "certificate_id": {
-            "$ref": "#/definitions/certificate_id"
-          },
-          "ssl_forced": {
-            "$ref": "#/definitions/ssl_forced"
-          },
-          "hsts_enabled": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "hsts_subdomains": {
-            "$ref": "#/definitions/hsts_enabled"
-          },
-          "http2_support": {
-            "$ref": "#/definitions/http2_support"
-          },
-          "block_exploits": {
-            "$ref": "#/definitions/block_exploits"
-          },
-          "advanced_config": {
-            "$ref": "#/definitions/advanced_config"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing Redirection Host",
-      "href": "/nginx/redirection-hosts/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Enable",
-      "description": "Enables a existing Redirection Host",
-      "href": "/nginx/redirection-hosts/{definitions.identity.example}/enable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Disable",
-      "description": "Disables a existing Redirection Host",
-      "href": "/nginx/redirection-hosts/{definitions.identity.example}/disable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/settings.json b/backend/schema/endpoints/settings.json
deleted file mode 100644
index 29e2865ae..000000000
--- a/backend/schema/endpoints/settings.json
+++ /dev/null
@@ -1,99 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/settings",
-  "title": "Settings",
-  "description": "Endpoints relating to Settings",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/setting_id"
-    },
-    "name": {
-      "description": "Name",
-      "example": "Default Site",
-      "type": "string",
-      "minLength": 2,
-      "maxLength": 100
-    },
-    "description": {
-      "description": "Description",
-      "example": "Default Site",
-      "type": "string",
-      "minLength": 2,
-      "maxLength": 255
-    },
-    "value": {
-      "description": "Value",
-      "example": "404",
-      "type": "string",
-      "maxLength": 255
-    },
-    "meta": {
-      "type": "object"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Settings",
-      "href": "/settings",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing Setting",
-      "href": "/settings/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "properties": {
-          "value": {
-            "$ref": "#/definitions/value"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    }
-  ],
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "name": {
-      "$ref": "#/definitions/description"
-    },
-    "description": {
-      "$ref": "#/definitions/description"
-    },
-    "value": {
-      "$ref": "#/definitions/value"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    }
-  }
-}
diff --git a/backend/schema/endpoints/streams.json b/backend/schema/endpoints/streams.json
deleted file mode 100644
index 159c8036e..000000000
--- a/backend/schema/endpoints/streams.json
+++ /dev/null
@@ -1,234 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/streams",
-  "title": "Streams",
-  "description": "Endpoints relating to Streams",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "incoming_port": {
-      "type": "integer",
-      "minimum": 1,
-      "maximum": 65535
-    },
-    "forwarding_host": {
-      "anyOf": [
-        {
-          "$ref": "../definitions.json#/definitions/domain_name"
-        },
-        {
-          "type": "string",
-          "format": "ipv4"
-        },
-        {
-          "type": "string",
-          "format": "ipv6"
-        }
-      ]
-    },
-    "forwarding_port": {
-      "type": "integer",
-      "minimum": 1,
-      "maximum": 65535
-    },
-    "tcp_forwarding": {
-      "type": "boolean"
-    },
-    "udp_forwarding": {
-      "type": "boolean"
-    },
-    "enabled": {
-      "$ref": "../definitions.json#/definitions/enabled"
-    },
-    "meta": {
-      "type": "object"
-    }
-  },
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "incoming_port": {
-      "$ref": "#/definitions/incoming_port"
-    },
-    "forwarding_host": {
-      "$ref": "#/definitions/forwarding_host"
-    },
-    "forwarding_port": {
-      "$ref": "#/definitions/forwarding_port"
-    },
-    "tcp_forwarding": {
-      "$ref": "#/definitions/tcp_forwarding"
-    },
-    "udp_forwarding": {
-      "$ref": "#/definitions/udp_forwarding"
-    },
-    "enabled": {
-      "$ref": "#/definitions/enabled"
-    },
-    "meta": {
-      "$ref": "#/definitions/meta"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Steams",
-      "href": "/nginx/streams",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new Stream",
-      "href": "/nginx/streams",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "required": [
-          "incoming_port",
-          "forwarding_host",
-          "forwarding_port"
-        ],
-        "properties": {
-          "incoming_port": {
-            "$ref": "#/definitions/incoming_port"
-          },
-          "forwarding_host": {
-            "$ref": "#/definitions/forwarding_host"
-          },
-          "forwarding_port": {
-            "$ref": "#/definitions/forwarding_port"
-          },
-          "tcp_forwarding": {
-            "$ref": "#/definitions/tcp_forwarding"
-          },
-          "udp_forwarding": {
-            "$ref": "#/definitions/udp_forwarding"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing Stream",
-      "href": "/nginx/streams/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "incoming_port": {
-            "$ref": "#/definitions/incoming_port"
-          },
-          "forwarding_host": {
-            "$ref": "#/definitions/forwarding_host"
-          },
-          "forwarding_port": {
-            "$ref": "#/definitions/forwarding_port"
-          },
-          "tcp_forwarding": {
-            "$ref": "#/definitions/tcp_forwarding"
-          },
-          "udp_forwarding": {
-            "$ref": "#/definitions/udp_forwarding"
-          },
-          "meta": {
-            "$ref": "#/definitions/meta"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing Stream",
-      "href": "/nginx/streams/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Enable",
-      "description": "Enables a existing Stream",
-      "href": "/nginx/streams/{definitions.identity.example}/enable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Disable",
-      "description": "Disables a existing Stream",
-      "href": "/nginx/streams/{definitions.identity.example}/disable",
-      "access": "private",
-      "method": "POST",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/tokens.json b/backend/schema/endpoints/tokens.json
deleted file mode 100644
index 920af63f4..000000000
--- a/backend/schema/endpoints/tokens.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/tokens",
-  "title": "Token",
-  "description": "Tokens are required to authenticate against the API",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "identity": {
-      "description": "Email Address or other 3rd party providers identifier",
-      "example": "john@example.com",
-      "type": "string"
-    },
-    "secret": {
-      "description": "A password or key",
-      "example": "correct horse battery staple",
-      "type": "string"
-    },
-    "token": {
-      "description": "JWT",
-      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
-      "type": "string"
-    },
-    "expires": {
-      "description": "Token expiry time",
-      "format": "date-time",
-      "type": "string"
-    },
-    "scope": {
-      "description": "Scope of the Token, defaults to 'user'",
-      "example": "user",
-      "type": "string"
-    }
-  },
-  "links": [
-    {
-      "title": "Create",
-      "description": "Creates a new token.",
-      "href": "/tokens",
-      "access": "public",
-      "method": "POST",
-      "rel": "create",
-      "schema": {
-        "type": "object",
-        "required": [
-          "identity",
-          "secret"
-        ],
-        "properties": {
-          "identity": {
-            "$ref": "#/definitions/identity"
-          },
-          "secret": {
-            "$ref": "#/definitions/secret"
-          },
-          "scope": {
-            "$ref": "#/definitions/scope"
-          }
-        }
-      },
-      "targetSchema": {
-        "type": "object",
-        "properties": {
-          "token": {
-            "$ref": "#/definitions/token"
-          },
-          "expires": {
-            "$ref": "#/definitions/expires"
-          }
-        }
-      }
-    },
-    {
-      "title": "Refresh",
-      "description": "Returns a new token.",
-      "href": "/tokens",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {},
-      "targetSchema": {
-        "type": "object",
-        "properties": {
-          "token": {
-            "$ref": "#/definitions/token"
-          },
-          "expires": {
-            "$ref": "#/definitions/expires"
-          },
-          "scope": {
-            "$ref": "#/definitions/scope"
-          }
-        }
-      }
-    }
-  ]
-}
diff --git a/backend/schema/endpoints/users.json b/backend/schema/endpoints/users.json
deleted file mode 100644
index 42f44eac7..000000000
--- a/backend/schema/endpoints/users.json
+++ /dev/null
@@ -1,287 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "endpoints/users",
-  "title": "Users",
-  "description": "Endpoints relating to Users",
-  "stability": "stable",
-  "type": "object",
-  "definitions": {
-    "id": {
-      "$ref": "../definitions.json#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "../definitions.json#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "../definitions.json#/definitions/modified_on"
-    },
-    "name": {
-      "description": "Name",
-      "example": "Jamie Curnow",
-      "type": "string",
-      "minLength": 2,
-      "maxLength": 100
-    },
-    "nickname": {
-      "description": "Nickname",
-      "example": "Jamie",
-      "type": "string",
-      "minLength": 2,
-      "maxLength": 50
-    },
-    "email": {
-      "$ref": "../definitions.json#/definitions/email"
-    },
-    "avatar": {
-      "description": "Avatar",
-      "example": "http://somewhere.jpg",
-      "type": "string",
-      "minLength": 2,
-      "maxLength": 150,
-      "readOnly": true
-    },
-    "roles": {
-      "description": "Roles",
-      "example": [
-        "admin"
-      ],
-      "type": "array"
-    },
-    "is_disabled": {
-      "description": "Is Disabled",
-      "example": false,
-      "type": "boolean"
-    }
-  },
-  "links": [
-    {
-      "title": "List",
-      "description": "Returns a list of Users",
-      "href": "/users",
-      "access": "private",
-      "method": "GET",
-      "rel": "self",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "array",
-        "items": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Create",
-      "description": "Creates a new User",
-      "href": "/users",
-      "access": "private",
-      "method": "POST",
-      "rel": "create",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "required": [
-          "name",
-          "nickname",
-          "email"
-        ],
-        "properties": {
-          "name": {
-            "$ref": "#/definitions/name"
-          },
-          "nickname": {
-            "$ref": "#/definitions/nickname"
-          },
-          "email": {
-            "$ref": "#/definitions/email"
-          },
-          "roles": {
-            "$ref": "#/definitions/roles"
-          },
-          "is_disabled": {
-            "$ref": "#/definitions/is_disabled"
-          },
-          "auth": {
-            "type": "object",
-            "description": "Auth Credentials",
-            "example": {
-              "type": "password",
-              "secret": "bigredhorsebanana"
-            }
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Update",
-      "description": "Updates a existing User",
-      "href": "/users/{definitions.identity.example}",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "properties": {
-          "name": {
-            "$ref": "#/definitions/name"
-          },
-          "nickname": {
-            "$ref": "#/definitions/nickname"
-          },
-          "email": {
-            "$ref": "#/definitions/email"
-          },
-          "roles": {
-            "$ref": "#/definitions/roles"
-          },
-          "is_disabled": {
-            "$ref": "#/definitions/is_disabled"
-          }
-        }
-      },
-      "targetSchema": {
-        "properties": {
-          "$ref": "#/properties"
-        }
-      }
-    },
-    {
-      "title": "Delete",
-      "description": "Deletes a existing User",
-      "href": "/users/{definitions.identity.example}",
-      "access": "private",
-      "method": "DELETE",
-      "rel": "delete",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Set Password",
-      "description": "Sets a password for an existing User",
-      "href": "/users/{definitions.identity.example}/auth",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "required": [
-          "type",
-          "secret"
-        ],
-        "properties": {
-          "type": {
-            "type": "string",
-            "pattern": "^password$"
-          },
-          "current": {
-            "type": "string",
-            "minLength": 1,
-            "maxLength": 64
-          },
-          "secret": {
-            "type": "string",
-            "minLength": 8,
-            "maxLength": 64
-          }
-        }
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    },
-    {
-      "title": "Set Permissions",
-      "description": "Sets Permissions for a User",
-      "href": "/users/{definitions.identity.example}/permissions",
-      "access": "private",
-      "method": "PUT",
-      "rel": "update",
-      "http_header": {
-        "$ref": "../examples.json#/definitions/auth_header"
-      },
-      "schema": {
-        "type": "object",
-        "properties": {
-          "visibility": {
-            "type": "string",
-            "pattern": "^(all|user)$"
-          },
-          "access_lists": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          },
-          "dead_hosts": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          },
-          "proxy_hosts": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          },
-          "redirection_hosts": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          },
-          "streams": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          },
-          "certificates": {
-            "type": "string",
-            "pattern": "^(hidden|view|manage)$"
-          }
-        }
-      },
-      "targetSchema": {
-        "type": "boolean"
-      }
-    }
-  ],
-  "properties": {
-    "id": {
-      "$ref": "#/definitions/id"
-    },
-    "created_on": {
-      "$ref": "#/definitions/created_on"
-    },
-    "modified_on": {
-      "$ref": "#/definitions/modified_on"
-    },
-    "name": {
-      "$ref": "#/definitions/name"
-    },
-    "nickname": {
-      "$ref": "#/definitions/nickname"
-    },
-    "email": {
-      "$ref": "#/definitions/email"
-    },
-    "avatar": {
-      "$ref": "#/definitions/avatar"
-    },
-    "roles": {
-      "$ref": "#/definitions/roles"
-    },
-    "is_disabled": {
-      "$ref": "#/definitions/is_disabled"
-    }
-  }
-}
diff --git a/backend/schema/examples.json b/backend/schema/examples.json
deleted file mode 100644
index 37bc6c4d3..000000000
--- a/backend/schema/examples.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "examples",
-  "type": "object",
-  "definitions": {
-    "name": {
-      "description": "Name",
-      "example": "John Smith",
-      "type": "string",
-      "minLength": 1,
-      "maxLength": 255
-    },
-    "auth_header": {
-      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
-      "X-API-Version": "next"
-    },
-    "token": {
-      "type": "string",
-      "description": "JWT",
-      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk"
-    }
-  }
-}
diff --git a/backend/schema/index.js b/backend/schema/index.js
new file mode 100644
index 000000000..87b75f257
--- /dev/null
+++ b/backend/schema/index.js
@@ -0,0 +1,41 @@
+const refParser = require('@apidevtools/json-schema-ref-parser');
+
+let compiledSchema = null;
+
+module.exports = {
+
+	/**
+	 * Compiles the schema, by dereferencing it, only once
+	 * and returns the memory cached value
+	 */
+	getCompiledSchema: async () => {
+		if (compiledSchema === null) {
+			compiledSchema = await refParser.dereference(__dirname + '/swagger.json', {
+				mutateInputSchema: false,
+			});
+		}
+		return compiledSchema;
+	},
+
+	/**
+	 * Scans the schema for the validation schema for the given path and method
+	 * and returns it.
+	 *
+	 * @param {string} path
+	 * @param {string} method
+	 * @returns string|null
+	 */
+	getValidationSchema: (path, method) => {
+		if (compiledSchema !== null &&
+			typeof compiledSchema.paths[path] !== 'undefined' &&
+			typeof compiledSchema.paths[path][method] !== 'undefined' &&
+			typeof compiledSchema.paths[path][method].requestBody !== 'undefined' &&
+			typeof compiledSchema.paths[path][method].requestBody.content !== 'undefined' &&
+			typeof compiledSchema.paths[path][method].requestBody.content['application/json'] !== 'undefined' &&
+			typeof compiledSchema.paths[path][method].requestBody.content['application/json'].schema !== 'undefined'
+		) {
+			return compiledSchema.paths[path][method].requestBody.content['application/json'].schema;
+		}
+		return null;
+	}
+};
diff --git a/backend/schema/index.json b/backend/schema/index.json
deleted file mode 100644
index 6e7d1c8af..000000000
--- a/backend/schema/index.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "$id": "root",
-  "title": "Nginx Proxy Manager REST API",
-  "description": "This is the Nginx Proxy Manager REST API",
-  "version": "2.0.0",
-  "links": [
-    {
-      "href": "http://npm.example.com/api",
-      "rel": "self"
-    }
-  ],
-  "properties": {
-    "tokens": {
-      "$ref": "endpoints/tokens.json"
-    },
-    "users": {
-      "$ref": "endpoints/users.json"
-    },
-    "proxy-hosts": {
-      "$ref": "endpoints/proxy-hosts.json"
-    },
-    "redirection-hosts": {
-      "$ref": "endpoints/redirection-hosts.json"
-    },
-    "dead-hosts": {
-      "$ref": "endpoints/dead-hosts.json"
-    },
-    "streams": {
-      "$ref": "endpoints/streams.json"
-    },
-    "certificates": {
-      "$ref": "endpoints/certificates.json"
-    },
-    "access-lists": {
-      "$ref": "endpoints/access-lists.json"
-    },
-    "settings": {
-      "$ref": "endpoints/settings.json"
-    }
-  }
-}
diff --git a/backend/schema/paths/audit-log/get.json b/backend/schema/paths/audit-log/get.json
new file mode 100644
index 000000000..bc43e29dd
--- /dev/null
+++ b/backend/schema/paths/audit-log/get.json
@@ -0,0 +1,53 @@
+{
+	"operationId": "getAuditLog",
+	"summary": "Get Audit Log",
+	"tags": ["Audit Log"],
+	"security": [
+		{
+			"BearerAuth": ["audit-log"]
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 7,
+									"created_on": "2024-10-08T13:09:54.000Z",
+									"modified_on": "2024-10-08T13:09:54.000Z",
+									"user_id": 1,
+									"object_type": "user",
+									"object_id": 3,
+									"action": "updated",
+									"meta": {
+										"name": "John Doe",
+										"permissions": {
+											"user_id": 3,
+											"visibility": "all",
+											"access_lists": "manage",
+											"dead_hosts": "hidden",
+											"proxy_hosts": "manage",
+											"redirection_hosts": "view",
+											"streams": "hidden",
+											"certificates": "manage",
+											"id": 3,
+											"modified_on": "2024-10-08T13:09:54.000Z",
+											"created_on": "2024-10-08T13:09:51.000Z"
+										}
+									}
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../components/audit-log-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/get.json b/backend/schema/paths/get.json
new file mode 100644
index 000000000..8c3a4e025
--- /dev/null
+++ b/backend/schema/paths/get.json
@@ -0,0 +1,29 @@
+{
+	"operationId": "health",
+	"summary": "Returns the API health status",
+	"tags": ["Public"],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"status": "OK",
+								"version": {
+									"major": 2,
+									"minor": 1,
+									"revision": 0
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../components/health-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/access-lists/get.json b/backend/schema/paths/nginx/access-lists/get.json
new file mode 100644
index 000000000..a8b9adc69
--- /dev/null
+++ b/backend/schema/paths/nginx/access-lists/get.json
@@ -0,0 +1,50 @@
+{
+	"operationId": "getAccessLists",
+	"summary": "Get all access lists",
+	"tags": ["Access Lists"],
+	"security": [
+		{
+			"BearerAuth": ["access_lists"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["owner", "items", "clients", "proxy_hosts"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2024-10-08T22:15:40.000Z",
+									"modified_on": "2024-10-08T22:15:40.000Z",
+									"owner_user_id": 1,
+									"name": "test1234",
+									"meta": {},
+									"satisfy_any": true,
+									"pass_auth": false,
+									"proxy_host_count": 0
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/access-list-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/access-lists/listID/delete.json b/backend/schema/paths/nginx/access-lists/listID/delete.json
new file mode 100644
index 000000000..073585c8b
--- /dev/null
+++ b/backend/schema/paths/nginx/access-lists/listID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteAccessList",
+	"summary": "Delete a Access List",
+	"tags": ["Access Lists"],
+	"security": [
+		{
+			"BearerAuth": ["access_lists"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "listID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/access-lists/listID/get.json b/backend/schema/paths/nginx/access-lists/listID/get.json
new file mode 100644
index 000000000..e67023f89
--- /dev/null
+++ b/backend/schema/paths/nginx/access-lists/listID/get.json
@@ -0,0 +1,49 @@
+{
+	"operationId": "getAccessList",
+	"summary": "Get a access List",
+	"tags": ["Access Lists"],
+	"security": [
+		{
+			"BearerAuth": ["access_lists"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "listID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2020-01-30T09:36:08.000Z",
+								"modified_on": "2020-01-30T09:41:04.000Z",
+								"is_disabled": false,
+								"email": "jc@jc21.com",
+								"name": "Jamie Curnow",
+								"nickname": "James",
+								"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+								"roles": ["admin"]
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/access-list-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/access-lists/listID/put.json b/backend/schema/paths/nginx/access-lists/listID/put.json
new file mode 100644
index 000000000..7f887dad6
--- /dev/null
+++ b/backend/schema/paths/nginx/access-lists/listID/put.json
@@ -0,0 +1,163 @@
+{
+	"operationId": "updateAccessList",
+	"summary": "Update a Access List",
+	"tags": ["Access Lists"],
+	"security": [
+		{
+			"BearerAuth": ["access_lists"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "listID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Access List Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"name": {
+							"$ref": "../../../../components/access-list-object.json#/properties/name"
+						},
+						"satisfy_any": {
+							"$ref": "../../../../components/access-list-object.json#/properties/satisfy_any"
+						},
+						"pass_auth": {
+							"$ref": "../../../../components/access-list-object.json#/properties/pass_auth"
+						},
+						"items": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"additionalProperties": false,
+								"properties": {
+									"username": {
+										"type": "string",
+										"minLength": 1
+									},
+									"password": {
+										"type": "string"
+									}
+								}
+							}
+						},
+						"clients": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"additionalProperties": false,
+								"properties": {
+									"address": {
+										"oneOf": [
+											{
+												"type": "string",
+												"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+											},
+											{
+												"type": "string",
+												"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+											},
+											{
+												"type": "string",
+												"pattern": "^all$"
+											}
+										]
+									},
+									"directive": {
+										"$ref": "../../../../components/access-list-object.json#/properties/directive"
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-08T22:15:40.000Z",
+								"modified_on": "2024-10-08T22:34:34.000Z",
+								"owner_user_id": 1,
+								"name": "test123!!",
+								"meta": {},
+								"satisfy_any": true,
+								"pass_auth": false,
+								"proxy_host_count": 0,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-07T22:43:55.000Z",
+									"modified_on": "2024-10-08T12:52:54.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "some guy",
+									"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
+									"roles": ["admin"]
+								},
+								"items": [
+									{
+										"id": 1,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"username": "admin",
+										"password": "",
+										"meta": {},
+										"hint": "a****"
+									},
+									{
+										"id": 2,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"username": "asdad",
+										"password": "",
+										"meta": {},
+										"hint": "a*****"
+									}
+								],
+								"clients": [
+									{
+										"id": 1,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"address": "127.0.0.1",
+										"directive": "allow",
+										"meta": {}
+									}
+								],
+								"proxy_hosts": []
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/access-list-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/access-lists/post.json b/backend/schema/paths/nginx/access-lists/post.json
new file mode 100644
index 000000000..4c5a4edd2
--- /dev/null
+++ b/backend/schema/paths/nginx/access-lists/post.json
@@ -0,0 +1,155 @@
+{
+	"operationId": "createAccessList",
+	"summary": "Create a Access List",
+	"tags": ["Access Lists"],
+	"security": [
+		{
+			"BearerAuth": ["access_lists"]
+		}
+	],
+	"requestBody": {
+		"description": "Access List Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["name"],
+					"properties": {
+						"name": {
+							"$ref": "../../../components/access-list-object.json#/properties/name"
+						},
+						"satisfy_any": {
+							"$ref": "../../../components/access-list-object.json#/properties/satisfy_any"
+						},
+						"pass_auth": {
+							"$ref": "../../../components/access-list-object.json#/properties/pass_auth"
+						},
+						"items": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"additionalProperties": false,
+								"properties": {
+									"username": {
+										"type": "string",
+										"minLength": 1
+									},
+									"password": {
+										"type": "string",
+										"minLength": 1
+									}
+								}
+							}
+						},
+						"clients": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"additionalProperties": false,
+								"properties": {
+									"address": {
+										"oneOf": [
+											{
+												"type": "string",
+												"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+											},
+											{
+												"type": "string",
+												"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+											},
+											{
+												"type": "string",
+												"pattern": "^all$"
+											}
+										]
+									},
+									"directive": {
+										"$ref": "../../../components/access-list-object.json#/properties/directive"
+									}
+								}
+							}
+						},
+						"meta": {
+							"$ref": "../../../components/access-list-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-08T22:15:40.000Z",
+								"modified_on": "2024-10-08T22:15:40.000Z",
+								"owner_user_id": 1,
+								"name": "test1234",
+								"meta": {},
+								"satisfy_any": true,
+								"pass_auth": false,
+								"proxy_host_count": 0,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-07T22:43:55.000Z",
+									"modified_on": "2024-10-08T12:52:54.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "some guy",
+									"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
+									"roles": ["admin"]
+								},
+								"items": [
+									{
+										"id": 1,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"username": "admin",
+										"password": "",
+										"meta": {},
+										"hint": "a****"
+									},
+									{
+										"id": 2,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"username": "asdad",
+										"password": "",
+										"meta": {},
+										"hint": "a*****"
+									}
+								],
+								"proxy_hosts": [],
+								"clients": [
+									{
+										"id": 1,
+										"created_on": "2024-10-08T22:15:40.000Z",
+										"modified_on": "2024-10-08T22:15:40.000Z",
+										"access_list_id": 1,
+										"address": "127.0.0.1",
+										"directive": "allow",
+										"meta": {}
+									}
+								]
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/access-list-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/certID/delete.json b/backend/schema/paths/nginx/certificates/certID/delete.json
new file mode 100644
index 000000000..0d40bcb8c
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/certID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteCertificate",
+	"summary": "Delete a Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "certID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/certID/download/get.json b/backend/schema/paths/nginx/certificates/certID/download/get.json
new file mode 100644
index 000000000..4b858cae7
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/certID/download/get.json
@@ -0,0 +1,35 @@
+{
+	"operationId": "downloadCertificate",
+	"summary": "Downloads a Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "certID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/zip": {
+					"schema": {
+						"type": "string",
+						"format": "binary"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/certID/get.json b/backend/schema/paths/nginx/certificates/certID/get.json
new file mode 100644
index 000000000..22317b337
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/certID/get.json
@@ -0,0 +1,53 @@
+{
+	"operationId": "getCertificate",
+	"summary": "Get a Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "certID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 4,
+								"created_on": "2024-10-09T05:31:58.000Z",
+								"modified_on": "2024-10-09T05:32:11.000Z",
+								"owner_user_id": 1,
+								"provider": "letsencrypt",
+								"nice_name": "test.example.com",
+								"domain_names": ["test.example.com"],
+								"expires_on": "2025-01-07T04:34:18.000Z",
+								"meta": {
+									"letsencrypt_email": "jc@jc21.com",
+									"letsencrypt_agree": true,
+									"dns_challenge": false
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/certificate-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/certID/renew/post.json b/backend/schema/paths/nginx/certificates/certID/renew/post.json
new file mode 100644
index 000000000..ef4d20e5b
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/certID/renew/post.json
@@ -0,0 +1,54 @@
+{
+	"operationId": "renewCertificate",
+	"summary": "Renews a Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "certID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"expires_on": "2025-01-07T06:41:58.000Z",
+								"modified_on": "2024-10-09T07:39:51.000Z",
+								"id": 4,
+								"created_on": "2024-10-09T05:31:58.000Z",
+								"owner_user_id": 1,
+								"is_deleted": false,
+								"provider": "letsencrypt",
+								"nice_name": "My Test Cert",
+								"domain_names": ["test.jc21.supernerd.pro"],
+								"meta": {
+									"letsencrypt_email": "jc@jc21.com",
+									"letsencrypt_agree": true,
+									"dns_challenge": false
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/certificate-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/certID/upload/post.json b/backend/schema/paths/nginx/certificates/certID/upload/post.json
new file mode 100644
index 000000000..f38b8102a
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/certID/upload/post.json
@@ -0,0 +1,82 @@
+{
+	"operationId": "uploadCertificate",
+	"summary": "Uploads a custom Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "certID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"requestBody": {
+		"description": "Certificate Files",
+		"required": true,
+		"content": {
+			"multipart/form-data": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["certificate", "certificate_key"],
+					"properties": {
+						"certificate": {
+							"type": "string"
+						},
+						"certificate_key": {
+							"type": "string"
+						},
+						"intermediate_certificate": {
+							"type": "string"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"certificate": "-----BEGIN CERTIFICATE-----\nMIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw\ngZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1\ncm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD\nDDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu\nb3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe\nbWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93\nQEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu\nGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+\n2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU\nQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB\nIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE\nOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB\n/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy\nl8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s\nVXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn\nATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt\nIImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u\nm+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV\nXxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp\n1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw\nhp8bJUp/QN7pnOVCDbjTQ+HVMXw=\n-----END CERTIFICATE-----\n",
+								"certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n"
+							}
+						}
+					},
+					"schema": {
+						"type": "object",
+						"additionalProperties": false,
+						"required": ["certificate", "certificate_key"],
+						"properties": {
+							"certificate": {
+								"type": "string",
+								"minLength": 1
+							},
+							"certificate_key": {
+								"type": "string",
+								"minLength": 1
+							},
+							"intermediate_certificate": {
+								"type": "string",
+								"minLength": 1
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/get.json b/backend/schema/paths/nginx/certificates/get.json
new file mode 100644
index 000000000..2f4b556af
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/get.json
@@ -0,0 +1,54 @@
+{
+	"operationId": "getCertificates",
+	"summary": "Get all certificates",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["owner"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 4,
+									"created_on": "2024-10-09T05:31:58.000Z",
+									"modified_on": "2024-10-09T05:32:11.000Z",
+									"owner_user_id": 1,
+									"provider": "letsencrypt",
+									"nice_name": "test.example.com",
+									"domain_names": ["test.example.com"],
+									"expires_on": "2025-01-07T04:34:18.000Z",
+									"meta": {
+										"letsencrypt_email": "jc@jc21.com",
+										"letsencrypt_agree": true,
+										"dns_challenge": false
+									}
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/certificate-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/post.json b/backend/schema/paths/nginx/certificates/post.json
new file mode 100644
index 000000000..5a3306c22
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/post.json
@@ -0,0 +1,97 @@
+{
+	"operationId": "createCertificate",
+	"summary": "Create a Certificate",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"requestBody": {
+		"description": "Certificate Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["provider"],
+					"properties": {
+						"provider": {
+							"$ref": "../../../components/certificate-object.json#/properties/provider"
+						},
+						"nice_name": {
+							"$ref": "../../../components/certificate-object.json#/properties/nice_name"
+						},
+						"domain_names": {
+							"$ref": "../../../components/certificate-object.json#/properties/domain_names"
+						},
+						"meta": {
+							"$ref": "../../../components/certificate-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"expires_on": "2025-01-07 04:30:17",
+								"modified_on": "2024-10-09 05:28:51",
+								"id": 5,
+								"created_on": "2024-10-09 05:28:35",
+								"owner_user_id": 1,
+								"is_deleted": false,
+								"provider": "letsencrypt",
+								"nice_name": "test.example.com",
+								"domain_names": ["test.example.com"],
+								"meta": {
+									"letsencrypt_email": "jc@jc21.com",
+									"letsencrypt_agree": true,
+									"dns_challenge": false,
+									"letsencrypt_certificate": {
+										"cn": "test.example.com",
+										"issuer": "C = US, O = Let's Encrypt, CN = E5",
+										"dates": {
+											"from": 1728448218,
+											"to": 1736224217
+										}
+									}
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/certificate-object.json"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Domains are invalid"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/test-http/get.json b/backend/schema/paths/nginx/certificates/test-http/get.json
new file mode 100644
index 000000000..2b9a8dd3b
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/test-http/get.json
@@ -0,0 +1,40 @@
+{
+	"operationId": "testHttpReach",
+	"summary": "Test HTTP Reachability",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "domains",
+			"description": "Expansions",
+			"required": true,
+			"schema": {
+				"type": "string",
+				"example": "[\"test.example.ord\",\"test.example.com\",\"nonexistent.example.com\"]"
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"test.example.org": "ok",
+								"test.example.com": "other:Invalid domain or IP",
+								"nonexistent.example.com": "404"
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/certificates/validate/post.json b/backend/schema/paths/nginx/certificates/validate/post.json
new file mode 100644
index 000000000..21eb325ef
--- /dev/null
+++ b/backend/schema/paths/nginx/certificates/validate/post.json
@@ -0,0 +1,114 @@
+{
+	"operationId": "validateCertificates",
+	"summary": "Validates given Custom Certificates",
+	"tags": ["Certificates"],
+	"security": [
+		{
+			"BearerAuth": ["certificates"]
+		}
+	],
+	"requestBody": {
+		"description": "Certificate Files",
+		"required": true,
+		"content": {
+			"multipart/form-data": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["certificate", "certificate_key"],
+					"properties": {
+						"certificate": {
+							"type": "string"
+						},
+						"certificate_key": {
+							"type": "string"
+						},
+						"intermediate_certificate": {
+							"type": "string"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"certificate": {
+									"cn": "mkcert",
+									"issuer": "O = mkcert development CA, OU = jc@jc-Laptop.local (John Doe), CN = mkcert jc@jc-Laptop.local (John Doe)",
+									"dates": {
+										"from": 1728458537,
+										"to": 1799479337
+									}
+								},
+								"certificate_key": true
+							}
+						}
+					},
+					"schema": {
+						"type": "object",
+						"additionalProperties": false,
+						"required": ["certificate", "certificate_key"],
+						"properties": {
+							"certificate": {
+								"type": "object",
+								"additionalProperties": false,
+								"required": ["cn", "issuer", "dates"],
+								"properties": {
+									"cn": {
+										"type": "string"
+									},
+									"issuer": {
+										"type": "string"
+									},
+									"dates": {
+										"type": "object",
+										"additionalProperties": false,
+										"required": ["from", "to"],
+										"properties": {
+											"from": {
+												"type": "integer"
+											},
+											"to": {
+												"type": "integer"
+											}
+										}
+									}
+								}
+							},
+							"certificate_key": {
+								"type": "boolean"
+							}
+						}
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Certificate is not valid"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/get.json b/backend/schema/paths/nginx/dead-hosts/get.json
new file mode 100644
index 000000000..8a11a3f66
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/get.json
@@ -0,0 +1,57 @@
+{
+	"operationId": "getDeadHosts",
+	"summary": "Get all 404 hosts",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["owner", "certificate"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2024-10-09T01:38:52.000Z",
+									"modified_on": "2024-10-09T01:38:52.000Z",
+									"owner_user_id": 1,
+									"domain_names": ["test.example.com"],
+									"certificate_id": 0,
+									"ssl_forced": false,
+									"advanced_config": "",
+									"meta": {
+										"nginx_online": true,
+										"nginx_err": null
+									},
+									"http2_support": false,
+									"enabled": true,
+									"hsts_enabled": false,
+									"hsts_subdomains": false
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/dead-host-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/delete.json b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json
new file mode 100644
index 000000000..f3aa81a5b
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteDeadHost",
+	"summary": "Delete a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json
new file mode 100644
index 000000000..2cdcecf4b
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "disableDeadHost",
+	"summary": "Disable a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already disabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json
new file mode 100644
index 000000000..ca3ce9fae
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "enableDeadHost",
+	"summary": "Enable a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already enabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/get.json b/backend/schema/paths/nginx/dead-hosts/hostID/get.json
new file mode 100644
index 000000000..47e2f8b12
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/get.json
@@ -0,0 +1,56 @@
+{
+	"operationId": "getDeadHost",
+	"summary": "Get a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:38:52.000Z",
+								"modified_on": "2024-10-09T01:38:52.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/dead-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/put.json b/backend/schema/paths/nginx/dead-hosts/hostID/put.json
new file mode 100644
index 000000000..f9505ed48
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/put.json
@@ -0,0 +1,108 @@
+{
+	"operationId": "updateDeadHost",
+	"summary": "Update a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "404 Host Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/domain_names"
+						},
+						"certificate_id": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/http2_support"
+						},
+						"advanced_config": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/advanced_config"
+						},
+						"meta": {
+							"$ref": "../../../../components/dead-host-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:38:52.000Z",
+								"modified_on": "2024-10-09T01:46:06.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T00:59:56.000Z",
+									"modified_on": "2024-10-09T00:59:56.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								},
+								"certificate": null
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/dead-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/dead-hosts/post.json b/backend/schema/paths/nginx/dead-hosts/post.json
new file mode 100644
index 000000000..c8bbb6932
--- /dev/null
+++ b/backend/schema/paths/nginx/dead-hosts/post.json
@@ -0,0 +1,93 @@
+{
+	"operationId": "create404Host",
+	"summary": "Create a 404 Host",
+	"tags": ["404 Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["dead_hosts"]
+		}
+	],
+	"requestBody": {
+		"description": "404 Host Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["domain_names"],
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../components/dead-host-object.json#/properties/domain_names"
+						},
+						"certificate_id": {
+							"$ref": "../../../components/dead-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../components/dead-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../components/dead-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../components/dead-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../components/dead-host-object.json#/properties/http2_support"
+						},
+						"advanced_config": {
+							"$ref": "../../../components/dead-host-object.json#/properties/advanced_config"
+						},
+						"meta": {
+							"$ref": "../../../components/dead-host-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:38:52.000Z",
+								"modified_on": "2024-10-09T01:38:52.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"advanced_config": "",
+								"meta": {},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"certificate": null,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T00:59:56.000Z",
+									"modified_on": "2024-10-09T00:59:56.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/dead-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json
new file mode 100644
index 000000000..1d9f63351
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/get.json
@@ -0,0 +1,65 @@
+{
+	"operationId": "getProxyHosts",
+	"summary": "Get all proxy hosts",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["access_list", "owner", "certificate"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2024-10-08T23:23:03.000Z",
+									"modified_on": "2024-10-08T23:23:04.000Z",
+									"owner_user_id": 1,
+									"domain_names": ["test.example.com"],
+									"forward_host": "127.0.0.1",
+									"forward_port": 8989,
+									"access_list_id": 0,
+									"certificate_id": 0,
+									"ssl_forced": false,
+									"caching_enabled": false,
+									"block_exploits": false,
+									"advanced_config": "",
+									"meta": {
+										"nginx_online": true,
+										"nginx_err": null
+									},
+									"allow_websocket_upgrade": false,
+									"http2_support": false,
+									"forward_scheme": "http",
+									"enabled": true,
+									"locations": null,
+									"hsts_enabled": false,
+									"hsts_subdomains": false
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/proxy-host-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json
new file mode 100644
index 000000000..991ef0e9e
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteProxyHost",
+	"summary": "Delete a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json
new file mode 100644
index 000000000..54ff8a663
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "disableProxyHost",
+	"summary": "Disable a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already disabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json
new file mode 100644
index 000000000..9f052de05
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "enableProxyHost",
+	"summary": "Enable a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already enabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json
new file mode 100644
index 000000000..5e10a9cfd
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json
@@ -0,0 +1,64 @@
+{
+	"operationId": "getProxyHost",
+	"summary": "Get a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-08T23:23:03.000Z",
+								"modified_on": "2024-10-08T23:26:38.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_host": "192.168.0.10",
+								"forward_port": 8989,
+								"access_list_id": 0,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"caching_enabled": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"allow_websocket_upgrade": false,
+								"http2_support": false,
+								"forward_scheme": "http",
+								"enabled": true,
+								"locations": null,
+								"hsts_enabled": false,
+								"hsts_subdomains": false
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/proxy-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
new file mode 100644
index 000000000..5cab6e752
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
@@ -0,0 +1,143 @@
+{
+	"operationId": "updateProxyHost",
+	"summary": "Update a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Proxy Host Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
+						},
+						"forward_scheme": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
+						},
+						"forward_host": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
+						},
+						"forward_port": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
+						},
+						"certificate_id": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
+						},
+						"block_exploits": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits"
+						},
+						"caching_enabled": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
+						},
+						"allow_websocket_upgrade": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
+						},
+						"access_list_id": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
+						},
+						"advanced_config": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config"
+						},
+						"enabled": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/enabled"
+						},
+						"meta": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/meta"
+						},
+						"locations": {
+							"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-08T23:23:03.000Z",
+								"modified_on": "2024-10-08T23:26:37.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_host": "192.168.0.10",
+								"forward_port": 8989,
+								"access_list_id": 0,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"caching_enabled": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"allow_websocket_upgrade": false,
+								"http2_support": false,
+								"forward_scheme": "http",
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-07T22:43:55.000Z",
+									"modified_on": "2024-10-08T12:52:54.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "some guy",
+									"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
+									"roles": ["admin"]
+								},
+								"certificate": null,
+								"access_list": null
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/proxy-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json
new file mode 100644
index 000000000..85455fb6b
--- /dev/null
+++ b/backend/schema/paths/nginx/proxy-hosts/post.json
@@ -0,0 +1,128 @@
+{
+	"operationId": "createProxyHost",
+	"summary": "Create a Proxy Host",
+	"tags": ["Proxy Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["proxy_hosts"]
+		}
+	],
+	"requestBody": {
+		"description": "Proxy Host Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["domain_names", "forward_scheme", "forward_host", "forward_port"],
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/domain_names"
+						},
+						"forward_scheme": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme"
+						},
+						"forward_host": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/forward_host"
+						},
+						"forward_port": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/forward_port"
+						},
+						"certificate_id": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
+						},
+						"block_exploits": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/block_exploits"
+						},
+						"caching_enabled": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled"
+						},
+						"allow_websocket_upgrade": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
+						},
+						"access_list_id": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/access_list_id"
+						},
+						"advanced_config": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/advanced_config"
+						},
+						"enabled": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/enabled"
+						},
+						"meta": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/meta"
+						},
+						"locations": {
+							"$ref": "../../../components/proxy-host-object.json#/properties/locations"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-08T23:23:03.000Z",
+								"modified_on": "2024-10-08T23:23:03.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_host": "127.0.0.1",
+								"forward_port": 8989,
+								"access_list_id": 0,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"caching_enabled": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {},
+								"allow_websocket_upgrade": false,
+								"http2_support": false,
+								"forward_scheme": "http",
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"certificate": null,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-07T22:43:55.000Z",
+									"modified_on": "2024-10-08T12:52:54.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "some guy",
+									"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
+									"roles": ["admin"]
+								},
+								"access_list": null
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/proxy-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/get.json b/backend/schema/paths/nginx/redirection-hosts/get.json
new file mode 100644
index 000000000..0b35e0fc4
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/get.json
@@ -0,0 +1,62 @@
+{
+	"operationId": "getRedirectionHosts",
+	"summary": "Get all Redirection hosts",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["owner", "certificate"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2024-10-09T01:13:12.000Z",
+									"modified_on": "2024-10-09T01:13:13.000Z",
+									"owner_user_id": 1,
+									"domain_names": ["test.example.com"],
+									"forward_domain_name": "something-else.com",
+									"preserve_path": false,
+									"certificate_id": 0,
+									"ssl_forced": false,
+									"block_exploits": false,
+									"advanced_config": "",
+									"meta": {
+										"nginx_online": true,
+										"nginx_err": null
+									},
+									"http2_support": false,
+									"enabled": true,
+									"hsts_enabled": false,
+									"hsts_subdomains": false,
+									"forward_scheme": "http",
+									"forward_http_code": 301
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/redirection-host-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json
new file mode 100644
index 000000000..7330f3623
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteRedirectionHost",
+	"summary": "Delete a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json
new file mode 100644
index 000000000..8433220d5
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "disableRedirectionHost",
+	"summary": "Disable a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already disabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json
new file mode 100644
index 000000000..bef53436b
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "enableRedirectionHost",
+	"summary": "Enable a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already enabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/get.json b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json
new file mode 100644
index 000000000..d780f874b
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json
@@ -0,0 +1,61 @@
+{
+	"operationId": "getRedirectionHost",
+	"summary": "Get a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:13:12.000Z",
+								"modified_on": "2024-10-09T01:13:13.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_domain_name": "something-else.com",
+								"preserve_path": false,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"forward_scheme": "http",
+								"forward_http_code": 301
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/redirection-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json
new file mode 100644
index 000000000..fd97cbfa8
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json
@@ -0,0 +1,128 @@
+{
+	"operationId": "updateRedirectionHost",
+	"summary": "Update a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "hostID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Redirection Host       Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/domain_names"
+						},
+						"forward_http_code": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/forward_http_code"
+						},
+						"forward_scheme": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/forward_scheme"
+						},
+						"forward_domain_name": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/forward_domain_name"
+						},
+						"preserve_path": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/preserve_path"
+						},
+						"certificate_id": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/http2_support"
+						},
+						"block_exploits": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/block_exploits"
+						},
+						"advanced_config": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/advanced_config"
+						},
+						"meta": {
+							"$ref": "../../../../components/redirection-host-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:13:12.000Z",
+								"modified_on": "2024-10-09T01:18:11.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_domain_name": "something-else.com",
+								"preserve_path": false,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"forward_scheme": "http",
+								"forward_http_code": 301,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T00:59:56.000Z",
+									"modified_on": "2024-10-09T00:59:56.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								},
+								"certificate": null
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/redirection-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json
new file mode 100644
index 000000000..5bfde2c38
--- /dev/null
+++ b/backend/schema/paths/nginx/redirection-hosts/post.json
@@ -0,0 +1,113 @@
+{
+	"operationId": "createRedirectionHost",
+	"summary": "Create a Redirection Host",
+	"tags": ["Redirection Hosts"],
+	"security": [
+		{
+			"BearerAuth": ["redirection_hosts"]
+		}
+	],
+	"requestBody": {
+		"description": "Redirection Host Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["domain_names", "forward_scheme", "forward_http_code", "forward_domain_name"],
+					"properties": {
+						"domain_names": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/domain_names"
+						},
+						"forward_http_code": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/forward_http_code"
+						},
+						"forward_scheme": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/forward_scheme"
+						},
+						"forward_domain_name": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/forward_domain_name"
+						},
+						"preserve_path": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/preserve_path"
+						},
+						"certificate_id": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/certificate_id"
+						},
+						"ssl_forced": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/ssl_forced"
+						},
+						"hsts_enabled": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/hsts_enabled"
+						},
+						"hsts_subdomains": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/hsts_subdomains"
+						},
+						"http2_support": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/http2_support"
+						},
+						"block_exploits": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/block_exploits"
+						},
+						"advanced_config": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/advanced_config"
+						},
+						"meta": {
+							"$ref": "../../../components/redirection-host-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T01:13:12.000Z",
+								"modified_on": "2024-10-09T01:13:12.000Z",
+								"owner_user_id": 1,
+								"domain_names": ["test.example.com"],
+								"forward_domain_name": "something-else.com",
+								"preserve_path": false,
+								"certificate_id": 0,
+								"ssl_forced": false,
+								"block_exploits": false,
+								"advanced_config": "",
+								"meta": {},
+								"http2_support": false,
+								"enabled": true,
+								"hsts_enabled": false,
+								"hsts_subdomains": false,
+								"forward_scheme": "http",
+								"forward_http_code": 301,
+								"certificate": null,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T00:59:56.000Z",
+									"modified_on": "2024-10-09T00:59:56.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/redirection-host-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json
new file mode 100644
index 000000000..17969ee4e
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/get.json
@@ -0,0 +1,56 @@
+{
+	"operationId": "getStreams",
+	"summary": "Get all streams",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["owner", "certificate"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2024-10-09T02:33:45.000Z",
+									"modified_on": "2024-10-09T02:33:45.000Z",
+									"owner_user_id": 1,
+									"incoming_port": 9090,
+									"forwarding_host": "router.internal",
+									"forwarding_port": 80,
+									"tcp_forwarding": true,
+									"udp_forwarding": false,
+									"meta": {
+										"nginx_online": true,
+										"nginx_err": null
+									},
+									"enabled": true,
+									"certificate_id": 0
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/stream-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json
new file mode 100644
index 000000000..d26996b69
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/post.json
@@ -0,0 +1,91 @@
+{
+	"operationId": "createStream",
+	"summary": "Create a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"requestBody": {
+		"description": "Stream Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["incoming_port", "forwarding_host", "forwarding_port"],
+					"properties": {
+						"incoming_port": {
+							"$ref": "../../../components/stream-object.json#/properties/incoming_port"
+						},
+						"forwarding_host": {
+							"$ref": "../../../components/stream-object.json#/properties/forwarding_host"
+						},
+						"forwarding_port": {
+							"$ref": "../../../components/stream-object.json#/properties/forwarding_port"
+						},
+						"tcp_forwarding": {
+							"$ref": "../../../components/stream-object.json#/properties/tcp_forwarding"
+						},
+						"udp_forwarding": {
+							"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
+						},
+						"certificate_id": {
+							"$ref": "../../../components/stream-object.json#/properties/certificate_id"
+						},
+						"meta": {
+							"$ref": "../../../components/stream-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T02:33:45.000Z",
+								"modified_on": "2024-10-09T02:33:45.000Z",
+								"owner_user_id": 1,
+								"incoming_port": 9090,
+								"forwarding_host": "router.internal",
+								"forwarding_port": 80,
+								"tcp_forwarding": true,
+								"udp_forwarding": false,
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"enabled": true,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T02:33:16.000Z",
+									"modified_on": "2024-10-09T02:33:16.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								},
+								"certificate_id": 0
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/stream-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/streamID/delete.json b/backend/schema/paths/nginx/streams/streamID/delete.json
new file mode 100644
index 000000000..3a9685258
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/streamID/delete.json
@@ -0,0 +1,39 @@
+{
+	"operationId": "deleteStream",
+	"summary": "Delete a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "streamID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/streamID/disable/post.json b/backend/schema/paths/nginx/streams/streamID/disable/post.json
new file mode 100644
index 000000000..d1c1b1c84
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/streamID/disable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "disableStream",
+	"summary": "Disable a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "streamID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already disabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/streamID/enable/post.json b/backend/schema/paths/nginx/streams/streamID/enable/post.json
new file mode 100644
index 000000000..dc914f5f2
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/streamID/enable/post.json
@@ -0,0 +1,59 @@
+{
+	"operationId": "enableStream",
+	"summary": "Enable a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "streamID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		},
+		"400": {
+			"description": "400 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"error": {
+									"code": 400,
+									"message": "Host is already enabled"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../../components/error.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json
new file mode 100644
index 000000000..801af13a7
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/streamID/get.json
@@ -0,0 +1,55 @@
+{
+	"operationId": "getStream",
+	"summary": "Get a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "streamID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T02:33:45.000Z",
+								"modified_on": "2024-10-09T02:33:45.000Z",
+								"owner_user_id": 1,
+								"incoming_port": 9090,
+								"forwarding_host": "router.internal",
+								"forwarding_port": 80,
+								"tcp_forwarding": true,
+								"udp_forwarding": false,
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"enabled": true,
+								"certificate_id": 0
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/stream-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json
new file mode 100644
index 000000000..14adb1631
--- /dev/null
+++ b/backend/schema/paths/nginx/streams/streamID/put.json
@@ -0,0 +1,103 @@
+{
+	"operationId": "updateStream",
+	"summary": "Update a Stream",
+	"tags": ["Streams"],
+	"security": [
+		{
+			"BearerAuth": ["streams"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "streamID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Stream Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"incoming_port": {
+							"$ref": "../../../../components/stream-object.json#/properties/incoming_port"
+						},
+						"forwarding_host": {
+							"$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
+						},
+						"forwarding_port": {
+							"$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
+						},
+						"tcp_forwarding": {
+							"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
+						},
+						"udp_forwarding": {
+							"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
+						},
+						"certificate_id": {
+							"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
+						},
+						"meta": {
+							"$ref": "../../../../components/stream-object.json#/properties/meta"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2024-10-09T02:33:45.000Z",
+								"modified_on": "2024-10-09T02:33:45.000Z",
+								"owner_user_id": 1,
+								"incoming_port": 9090,
+								"forwarding_host": "router.internal",
+								"forwarding_port": 80,
+								"tcp_forwarding": true,
+								"udp_forwarding": false,
+								"meta": {
+									"nginx_online": true,
+									"nginx_err": null
+								},
+								"enabled": true,
+								"owner": {
+									"id": 1,
+									"created_on": "2024-10-09T02:33:16.000Z",
+									"modified_on": "2024-10-09T02:33:16.000Z",
+									"is_deleted": false,
+									"is_disabled": false,
+									"email": "admin@example.com",
+									"name": "Administrator",
+									"nickname": "Admin",
+									"avatar": "",
+									"roles": ["admin"]
+								},
+								"certificate_id": 0
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../../components/stream-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/reports/hosts/get.json b/backend/schema/paths/reports/hosts/get.json
new file mode 100644
index 000000000..a40ddc723
--- /dev/null
+++ b/backend/schema/paths/reports/hosts/get.json
@@ -0,0 +1,50 @@
+{
+	"operationId": "reportsHosts",
+	"summary": "Report on Host Statistics",
+	"tags": ["Reports"],
+	"security": [
+		{
+			"BearerAuth": ["reports"]
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"proxy": 20,
+								"redirection": 1,
+								"stream": 0,
+								"dead": 1
+							}
+						}
+					},
+					"schema": {
+						"type": "object",
+						"properties": {
+							"proxy": {
+								"type": "integer",
+								"description": "Proxy Hosts Count"
+							},
+							"redirection": {
+								"type": "integer",
+								"description": "Redirection Hosts Count"
+							},
+							"stream": {
+								"type": "integer",
+								"description": "Streams Count"
+							},
+							"dead": {
+								"type": "integer",
+								"description": "404 Hosts Count"
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/schema/get.json b/backend/schema/paths/schema/get.json
new file mode 100644
index 000000000..d435b0042
--- /dev/null
+++ b/backend/schema/paths/schema/get.json
@@ -0,0 +1,10 @@
+{
+	"operationId": "schema",
+	"summary": "Returns this swagger API schema",
+	"tags": ["Public"],
+	"responses": {
+		"200": {
+			"description": "200 response"
+		}
+	}
+}
diff --git a/backend/schema/paths/settings/get.json b/backend/schema/paths/settings/get.json
new file mode 100644
index 000000000..5d148d8af
--- /dev/null
+++ b/backend/schema/paths/settings/get.json
@@ -0,0 +1,35 @@
+{
+	"operationId": "getSettings",
+	"summary": "Get all settings",
+	"tags": ["Settings"],
+	"security": [
+		{
+			"BearerAuth": ["settings"]
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": "default-site",
+									"name": "Default Site",
+									"description": "What to show when Nginx is hit with an unknown Host",
+									"value": "congratulations",
+									"meta": {}
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../components/setting-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/settings/settingID/get.json b/backend/schema/paths/settings/settingID/get.json
new file mode 100644
index 000000000..405b976d2
--- /dev/null
+++ b/backend/schema/paths/settings/settingID/get.json
@@ -0,0 +1,46 @@
+{
+	"operationId": "getSetting",
+	"summary": "Get a setting",
+	"tags": ["Settings"],
+	"security": [
+		{
+			"BearerAuth": ["settings"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "settingID",
+			"schema": {
+				"type": "string",
+				"minLength": 1
+			},
+			"required": true,
+			"description": "Setting ID",
+			"example": "default-site"
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": "default-site",
+								"name": "Default Site",
+								"description": "What to show when Nginx is hit with an unknown Host",
+								"value": "congratulations",
+								"meta": {}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/setting-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json
new file mode 100644
index 000000000..4ca624293
--- /dev/null
+++ b/backend/schema/paths/settings/settingID/put.json
@@ -0,0 +1,79 @@
+{
+	"operationId": "updateSetting",
+	"summary": "Update a setting",
+	"tags": ["Settings"],
+	"security": [
+		{
+			"BearerAuth": ["settings"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "settingID",
+			"schema": {
+				"type": "string",
+				"minLength": 1,
+				"enum": ["default-site"]
+			},
+			"required": true,
+			"description": "Setting ID",
+			"example": "default-site"
+		}
+	],
+	"requestBody": {
+		"description": "Setting Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"value": {
+							"type": "string",
+							"minLength": 1,
+							"enum": ["congratulations", "404", "444", "redirect", "html"]
+						},
+						"meta": {
+							"type": "object",
+							"additionalProperties": false,
+							"properties": {
+								"redirect": {
+									"type": "string"
+								},
+								"html": {
+									"type": "string"
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": "default-site",
+								"name": "Default Site",
+								"description": "What to show when Nginx is hit with an unknown Host",
+								"value": "congratulations",
+								"meta": {}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/setting-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/tokens/get.json b/backend/schema/paths/tokens/get.json
new file mode 100644
index 000000000..ef842eafe
--- /dev/null
+++ b/backend/schema/paths/tokens/get.json
@@ -0,0 +1,30 @@
+{
+	"operationId": "refreshToken",
+	"summary": "Refresh your access token",
+	"tags": ["Tokens"],
+	"security": [
+		{
+			"BearerAuth": ["tokens"]
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"expires": "2025-02-04T20:40:46.340Z",
+								"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../components/token-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/tokens/post.json b/backend/schema/paths/tokens/post.json
new file mode 100644
index 000000000..99703ff0d
--- /dev/null
+++ b/backend/schema/paths/tokens/post.json
@@ -0,0 +1,55 @@
+{
+	"operationId": "requestToken",
+	"summary": "Request a new access token from credentials",
+	"tags": ["Tokens"],
+	"requestBody": {
+		"description": "Credentials Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"additionalProperties": false,
+					"properties": {
+						"identity": {
+							"minLength": 1,
+							"type": "string"
+						},
+						"scope": {
+							"minLength": 1,
+							"type": "string",
+							"enum": ["user"]
+						},
+						"secret": {
+							"minLength": 1,
+							"type": "string"
+						}
+					},
+					"required": ["identity", "secret"],
+					"type": "object"
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"result": {
+									"expires": "2025-02-04T20:40:46.340Z",
+									"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../components/token-object.json"
+					}
+				}
+			},
+			"description": "200 response"
+		}
+	}
+}
diff --git a/backend/schema/paths/users/get.json b/backend/schema/paths/users/get.json
new file mode 100644
index 000000000..374153011
--- /dev/null
+++ b/backend/schema/paths/users/get.json
@@ -0,0 +1,74 @@
+{
+	"operationId": "getUsers",
+	"summary": "Get all users",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "query",
+			"name": "expand",
+			"description": "Expansions",
+			"schema": {
+				"type": "string",
+				"enum": ["permissions"]
+			}
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2020-01-30T09:36:08.000Z",
+									"modified_on": "2020-01-30T09:41:04.000Z",
+									"is_disabled": false,
+									"email": "jc@jc21.com",
+									"name": "Jamie Curnow",
+									"nickname": "James",
+									"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+									"roles": ["admin"]
+								}
+							]
+						},
+						"withPermissions": {
+							"value": [
+								{
+									"id": 1,
+									"created_on": "2020-01-30T09:36:08.000Z",
+									"modified_on": "2020-01-30T09:41:04.000Z",
+									"is_disabled": false,
+									"email": "jc@jc21.com",
+									"name": "Jamie Curnow",
+									"nickname": "James",
+									"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+									"roles": ["admin"],
+									"permissions": {
+										"visibility": "all",
+										"proxy_hosts": "manage",
+										"redirection_hosts": "manage",
+										"dead_hosts": "manage",
+										"streams": "manage",
+										"access_lists": "manage",
+										"certificates": "manage"
+									}
+								}
+							]
+						}
+					},
+					"schema": {
+						"$ref": "../../components/user-list.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/post.json b/backend/schema/paths/users/post.json
new file mode 100644
index 000000000..c0213fe05
--- /dev/null
+++ b/backend/schema/paths/users/post.json
@@ -0,0 +1,88 @@
+{
+	"operationId": "createUser",
+	"summary": "Create a User",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"requestBody": {
+		"description": "User Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"required": ["name", "nickname", "email"],
+					"properties": {
+						"name": {
+							"$ref": "../../components/user-object.json#/properties/name"
+						},
+						"nickname": {
+							"$ref": "../../components/user-object.json#/properties/nickname"
+						},
+						"email": {
+							"$ref": "../../components/user-object.json#/properties/email"
+						},
+						"roles": {
+							"$ref": "../../components/user-object.json#/properties/roles"
+						},
+						"is_disabled": {
+							"$ref": "../../components/user-object.json#/properties/is_disabled"
+						},
+						"auth": {
+							"type": "object",
+							"description": "Auth Credentials",
+							"example": {
+								"type": "password",
+								"secret": "bigredhorsebanana"
+							}
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"201": {
+			"description": "201 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 2,
+								"created_on": "2020-01-30T09:41:04.000Z",
+								"modified_on": "2020-01-30T09:41:04.000Z",
+								"is_disabled": false,
+								"email": "jc@jc21.com",
+								"name": "Jamie Curnow",
+								"nickname": "James",
+								"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+								"roles": ["admin"],
+								"permissions": {
+									"id": 3,
+									"created_on": "2020-01-30T09:41:04.000Z",
+									"modified_on": "2020-01-30T09:41:04.000Z",
+									"user_id": 2,
+									"visibility": "user",
+									"proxy_hosts": "manage",
+									"redirection_hosts": "manage",
+									"dead_hosts": "manage",
+									"streams": "manage",
+									"access_lists": "manage",
+									"certificates": "manage"
+								}
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../components/user-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/auth/put.json b/backend/schema/paths/users/userID/auth/put.json
new file mode 100644
index 000000000..a72f5617c
--- /dev/null
+++ b/backend/schema/paths/users/userID/auth/put.json
@@ -0,0 +1,79 @@
+{
+	"operationId": "updateUserAuth",
+	"summary": "Update a User's Authentication",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"oneOf": [
+					{
+						"type": "string",
+						"pattern": "^me$"
+					},
+					{
+						"type": "integer",
+						"minimum": 1
+					}
+				]
+			},
+			"required": true,
+			"description": "User ID or 'me' for yourself",
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Auth Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"required": ["type", "secret"],
+					"properties": {
+						"type": {
+							"type": "string",
+							"pattern": "^password$",
+							"example": "password"
+						},
+						"current": {
+							"type": "string",
+							"minLength": 1,
+							"maxLength": 64,
+							"example": "changeme"
+						},
+						"secret": {
+							"type": "string",
+							"minLength": 8,
+							"maxLength": 64,
+							"example": "mySuperN3wP@ssword!"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/delete.json b/backend/schema/paths/users/userID/delete.json
new file mode 100644
index 000000000..7d4f36151
--- /dev/null
+++ b/backend/schema/paths/users/userID/delete.json
@@ -0,0 +1,40 @@
+{
+	"operationId": "deleteUser",
+	"summary": "Delete a User",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"description": "User ID",
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/get.json b/backend/schema/paths/users/userID/get.json
new file mode 100644
index 000000000..cb8ac61b4
--- /dev/null
+++ b/backend/schema/paths/users/userID/get.json
@@ -0,0 +1,58 @@
+{
+	"operationId": "getUser",
+	"summary": "Get a user",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"oneOf": [
+					{
+						"type": "string",
+						"pattern": "^me$"
+					},
+					{
+						"type": "integer",
+						"minimum": 1
+					}
+				]
+			},
+			"required": true,
+			"description": "User ID or 'me' for yourself",
+			"example": 1
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 1,
+								"created_on": "2020-01-30T09:36:08.000Z",
+								"modified_on": "2020-01-30T09:41:04.000Z",
+								"is_disabled": false,
+								"email": "jc@jc21.com",
+								"name": "Jamie Curnow",
+								"nickname": "James",
+								"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+								"roles": ["admin"]
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/user-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/login/post.json b/backend/schema/paths/users/userID/login/post.json
new file mode 100644
index 000000000..6148d182b
--- /dev/null
+++ b/backend/schema/paths/users/userID/login/post.json
@@ -0,0 +1,73 @@
+{
+	"operationId": "loginAsUser",
+	"summary": "Login as this user",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"description": "User ID",
+			"example": 2
+		}
+	],
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg",
+								"expires": "2020-01-31T10:56:23.239Z",
+								"user": {
+									"id": 1,
+									"created_on": "2020-01-30T10:43:44.000Z",
+									"modified_on": "2020-01-30T10:43:44.000Z",
+									"is_disabled": false,
+									"email": "jc@jc21.com",
+									"name": "Jamie Curnow",
+									"nickname": "James",
+									"avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm",
+									"roles": ["admin"]
+								}
+							}
+						}
+					},
+					"schema": {
+						"type": "object",
+						"description": "Login object",
+						"required": ["expires", "token", "user"],
+						"additionalProperties": false,
+						"properties": {
+							"expires": {
+								"description": "Token Expiry Unix Time",
+								"example": 1566540249,
+								"minimum": 1,
+								"type": "number"
+							},
+							"token": {
+								"description": "JWT Token",
+								"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
+								"type": "string"
+							},
+							"user": {
+								"$ref": "../../../../components/user-object.json"
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/permissions/put.json b/backend/schema/paths/users/userID/permissions/put.json
new file mode 100644
index 000000000..2dcd2aed7
--- /dev/null
+++ b/backend/schema/paths/users/userID/permissions/put.json
@@ -0,0 +1,51 @@
+{
+	"operationId": "updateUserPermissions",
+	"summary": "Update a User's Permissions",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"type": "integer",
+				"minimum": 1
+			},
+			"required": true,
+			"description": "User ID",
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "Permissions Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"$ref": "../../../../components/permission-object.json"
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": true
+						}
+					},
+					"schema": {
+						"type": "boolean"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/paths/users/userID/put.json b/backend/schema/paths/users/userID/put.json
new file mode 100644
index 000000000..60a6cd132
--- /dev/null
+++ b/backend/schema/paths/users/userID/put.json
@@ -0,0 +1,88 @@
+{
+	"operationId": "updateUser",
+	"summary": "Update a User",
+	"tags": ["Users"],
+	"security": [
+		{
+			"BearerAuth": ["users"]
+		}
+	],
+	"parameters": [
+		{
+			"in": "path",
+			"name": "userID",
+			"schema": {
+				"oneOf": [
+					{
+						"type": "string",
+						"pattern": "^me$"
+					},
+					{
+						"type": "integer",
+						"minimum": 1
+					}
+				]
+			},
+			"required": true,
+			"description": "User ID or 'me' for yourself",
+			"example": 2
+		}
+	],
+	"requestBody": {
+		"description": "User Payload",
+		"required": true,
+		"content": {
+			"application/json": {
+				"schema": {
+					"type": "object",
+					"additionalProperties": false,
+					"minProperties": 1,
+					"properties": {
+						"name": {
+							"$ref": "../../../components/user-object.json#/properties/name"
+						},
+						"nickname": {
+							"$ref": "../../../components/user-object.json#/properties/nickname"
+						},
+						"email": {
+							"$ref": "../../../components/user-object.json#/properties/email"
+						},
+						"roles": {
+							"$ref": "../../../components/user-object.json#/properties/roles"
+						},
+						"is_disabled": {
+							"$ref": "../../../components/user-object.json#/properties/is_disabled"
+						}
+					}
+				}
+			}
+		}
+	},
+	"responses": {
+		"200": {
+			"description": "200 response",
+			"content": {
+				"application/json": {
+					"examples": {
+						"default": {
+							"value": {
+								"id": 2,
+								"created_on": "2020-01-30T09:36:08.000Z",
+								"modified_on": "2020-01-30T09:41:04.000Z",
+								"is_disabled": false,
+								"email": "jc@jc21.com",
+								"name": "Jamie Curnow",
+								"nickname": "James",
+								"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
+								"roles": ["admin"]
+							}
+						}
+					},
+					"schema": {
+						"$ref": "../../../components/user-object.json"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json
new file mode 100644
index 000000000..4a502b4e4
--- /dev/null
+++ b/backend/schema/swagger.json
@@ -0,0 +1,274 @@
+{
+	"openapi": "3.1.0",
+	"info": {
+		"title": "Nginx Proxy Manager API",
+		"version": "2.x.x"
+	},
+	"servers": [
+		{
+			"url": "http://127.0.0.1:81/api"
+		}
+	],
+	"components": {
+		"securitySchemes": {
+			"bearerAuth": {
+				"type": "http",
+				"scheme": "bearer",
+				"bearerFormat": "JWT"
+			}
+		}
+	},
+	"paths": {
+		"/": {
+			"get": {
+				"$ref": "./paths/get.json"
+			}
+		},
+		"/audit-log": {
+			"get": {
+				"$ref": "./paths/audit-log/get.json"
+			}
+		},
+		"/nginx/access-lists": {
+			"get": {
+				"$ref": "./paths/nginx/access-lists/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/access-lists/post.json"
+			}
+		},
+		"/nginx/access-lists/{listID}": {
+			"get": {
+				"$ref": "./paths/nginx/access-lists/listID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/nginx/access-lists/listID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/access-lists/listID/delete.json"
+			}
+		},
+		"/nginx/certificates": {
+			"get": {
+				"$ref": "./paths/nginx/certificates/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/certificates/post.json"
+			}
+		},
+		"/nginx/certificates/validate": {
+			"post": {
+				"$ref": "./paths/nginx/certificates/validate/post.json"
+			}
+		},
+		"/nginx/certificates/test-http": {
+			"get": {
+				"$ref": "./paths/nginx/certificates/test-http/get.json"
+			}
+		},
+		"/nginx/certificates/{certID}": {
+			"get": {
+				"$ref": "./paths/nginx/certificates/certID/get.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/certificates/certID/delete.json"
+			}
+		},
+		"/nginx/certificates/{certID}/download": {
+			"get": {
+				"$ref": "./paths/nginx/certificates/certID/download/get.json"
+			}
+		},
+		"/nginx/certificates/{certID}/renew": {
+			"post": {
+				"$ref": "./paths/nginx/certificates/certID/renew/post.json"
+			}
+		},
+		"/nginx/certificates/{certID}/upload": {
+			"post": {
+				"$ref": "./paths/nginx/certificates/certID/upload/post.json"
+			}
+		},
+		"/nginx/proxy-hosts": {
+			"get": {
+				"$ref": "./paths/nginx/proxy-hosts/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/proxy-hosts/post.json"
+			}
+		},
+		"/nginx/proxy-hosts/{hostID}": {
+			"get": {
+				"$ref": "./paths/nginx/proxy-hosts/hostID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/nginx/proxy-hosts/hostID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/proxy-hosts/hostID/delete.json"
+			}
+		},
+		"/nginx/proxy-hosts/{hostID}/enable": {
+			"post": {
+				"$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json"
+			}
+		},
+		"/nginx/proxy-hosts/{hostID}/disable": {
+			"post": {
+				"$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json"
+			}
+		},
+		"/nginx/redirection-hosts": {
+			"get": {
+				"$ref": "./paths/nginx/redirection-hosts/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/redirection-hosts/post.json"
+			}
+		},
+		"/nginx/redirection-hosts/{hostID}": {
+			"get": {
+				"$ref": "./paths/nginx/redirection-hosts/hostID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/nginx/redirection-hosts/hostID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/redirection-hosts/hostID/delete.json"
+			}
+		},
+		"/nginx/redirection-hosts/{hostID}/enable": {
+			"post": {
+				"$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json"
+			}
+		},
+		"/nginx/redirection-hosts/{hostID}/disable": {
+			"post": {
+				"$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json"
+			}
+		},
+		"/nginx/dead-hosts": {
+			"get": {
+				"$ref": "./paths/nginx/dead-hosts/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/dead-hosts/post.json"
+			}
+		},
+		"/nginx/dead-hosts/{hostID}": {
+			"get": {
+				"$ref": "./paths/nginx/dead-hosts/hostID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/nginx/dead-hosts/hostID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/dead-hosts/hostID/delete.json"
+			}
+		},
+		"/nginx/dead-hosts/{hostID}/enable": {
+			"post": {
+				"$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json"
+			}
+		},
+		"/nginx/dead-hosts/{hostID}/disable": {
+			"post": {
+				"$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json"
+			}
+		},
+		"/nginx/streams": {
+			"get": {
+				"$ref": "./paths/nginx/streams/get.json"
+			},
+			"post": {
+				"$ref": "./paths/nginx/streams/post.json"
+			}
+		},
+		"/nginx/streams/{streamID}": {
+			"get": {
+				"$ref": "./paths/nginx/streams/streamID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/nginx/streams/streamID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/nginx/streams/streamID/delete.json"
+			}
+		},
+		"/nginx/streams/{streamID}/enable": {
+			"post": {
+				"$ref": "./paths/nginx/streams/streamID/enable/post.json"
+			}
+		},
+		"/nginx/streams/{streamID}/disable": {
+			"post": {
+				"$ref": "./paths/nginx/streams/streamID/disable/post.json"
+			}
+		},
+		"/reports/hosts": {
+			"get": {
+				"$ref": "./paths/reports/hosts/get.json"
+			}
+		},
+		"/schema": {
+			"get": {
+				"$ref": "./paths/schema/get.json"
+			}
+		},
+		"/settings": {
+			"get": {
+				"$ref": "./paths/settings/get.json"
+			}
+		},
+		"/settings/{settingID}": {
+			"get": {
+				"$ref": "./paths/settings/settingID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/settings/settingID/put.json"
+			}
+		},
+		"/tokens": {
+			"get": {
+				"$ref": "./paths/tokens/get.json"
+			},
+			"post": {
+				"$ref": "./paths/tokens/post.json"
+			}
+		},
+		"/users": {
+			"get": {
+				"$ref": "./paths/users/get.json"
+			},
+			"post": {
+				"$ref": "./paths/users/post.json"
+			}
+		},
+		"/users/{userID}": {
+			"get": {
+				"$ref": "./paths/users/userID/get.json"
+			},
+			"put": {
+				"$ref": "./paths/users/userID/put.json"
+			},
+			"delete": {
+				"$ref": "./paths/users/userID/delete.json"
+			}
+		},
+		"/users/{userID}/auth": {
+			"put": {
+				"$ref": "./paths/users/userID/auth/put.json"
+			}
+		},
+		"/users/{userID}/permissions": {
+			"put": {
+				"$ref": "./paths/users/userID/permissions/put.json"
+			}
+		},
+		"/users/{userID}/login": {
+			"post": {
+				"$ref": "./paths/users/userID/login/post.json"
+			}
+		}
+	}
+}
diff --git a/backend/setup.js b/backend/setup.js
index 9a7b69705..6b9b8e78a 100644
--- a/backend/setup.js
+++ b/backend/setup.js
@@ -15,18 +15,18 @@ const certbot             = require('./lib/certbot');
 const setupDefaultUser = () => {
 	return userModel
 		.query()
-		.select(userModel.raw('COUNT(`id`) as `count`'))
+		.select('id', )
 		.where('is_deleted', 0)
 		.first()
 		.then((row) => {
-			if (!row.count) {
+			if (!row || !row.id) {
 				// Create a new user and set password
-				let email    = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com';
-				let password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme';
-				
+				const email    = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com';
+				const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme';
+
 				logger.info('Creating a new user: ' + email + ' with password: ' + password);
 
-				let data = {
+				const data = {
 					is_deleted: 0,
 					email:      email,
 					name:       'Administrator',
@@ -77,11 +77,11 @@ const setupDefaultUser = () => {
 const setupDefaultSettings = () => {
 	return settingModel
 		.query()
-		.select(settingModel.raw('COUNT(`id`) as `count`'))
+		.select('id')
 		.where({id: 'default-site'})
 		.first()
 		.then((row) => {
-			if (!row.count) {
+			if (!row || !row.id) {
 				settingModel
 					.query()
 					.insert({
diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf
index 447006c0c..f59263775 100644
--- a/backend/templates/_access.conf
+++ b/backend/templates/_access.conf
@@ -4,7 +4,7 @@
     auth_basic            "Authorization required";
     auth_basic_user_file  /data/access/{{ access_list_id }};
 
-    {% if access_list.pass_auth == 0 %}
+    {% if access_list.pass_auth == 0 or access_list.pass_auth == true %}
     proxy_set_header Authorization "";
     {% endif %}
 
@@ -17,7 +17,7 @@
     deny all;
 
     # Access checks must...
-    {% if access_list.satisfy_any == 1 %}
+    {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %}
     satisfy any;
     {% else %}
     satisfy all;
diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf
index 06ca7bb87..efcca5cd5 100644
--- a/backend/templates/_certificates.conf
+++ b/backend/templates/_certificates.conf
@@ -2,6 +2,7 @@
 {% if certificate.provider == "letsencrypt" %}
   # Let's Encrypt SSL
   include conf.d/include/letsencrypt-acme-challenge.conf;
+  include conf.d/include/ssl-cache.conf;
   include conf.d/include/ssl-ciphers.conf;
   ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
diff --git a/backend/templates/_certificates_stream.conf b/backend/templates/_certificates_stream.conf
new file mode 100644
index 000000000..ba7812fdd
--- /dev/null
+++ b/backend/templates/_certificates_stream.conf
@@ -0,0 +1,13 @@
+{% if certificate and certificate_id > 0 %}
+{% if certificate.provider == "letsencrypt" %}
+  # Let's Encrypt SSL
+  include conf.d/include/ssl-cache-stream.conf;
+  include conf.d/include/ssl-ciphers.conf;
+  ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
+  ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
+{%- else %}
+  # Custom SSL
+  ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
+  ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
+{%- endif -%}
+{%- endif -%}
diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf
index ad1c96ba0..34a808e6a 100644
--- a/backend/templates/_listen.conf
+++ b/backend/templates/_listen.conf
@@ -5,11 +5,16 @@
   #listen [::]:80;
 {% endif %}
 {% if certificate -%}
-  listen 443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %};
+  listen 443 ssl;
 {% if ipv6 -%}
-  listen [::]:443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %};
+  listen [::]:443 ssl;
 {% else -%}
   #listen [::]:443;
 {% endif %}
 {% endif %}
   server_name {{ domain_names | join: " " }};
+{% if http2_support == 1 or http2_support == true %}
+  http2 on;
+{% else -%}
+  http2 off;
+{% endif %}
\ No newline at end of file
diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf
index b75632278..a2ecb166d 100644
--- a/backend/templates/_location.conf
+++ b/backend/templates/_location.conf
@@ -6,6 +6,7 @@
     proxy_set_header X-Forwarded-Proto  $scheme;
     proxy_set_header X-Forwarded-For    $remote_addr;
     proxy_set_header X-Real-IP		$remote_addr;
+
     proxy_pass       {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
 
     {% include "_access.conf" %}
diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf
index 7a06469a9..2e7d2a007 100644
--- a/backend/templates/dead_host.conf
+++ b/backend/templates/dead_host.conf
@@ -22,5 +22,7 @@ server {
   }
 {% endif %}
 
+  # Custom
+  include /data/nginx/custom/server_dead[.]conf;
 }
 {% endif %}
diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf
index 76159a646..7333aaee1 100644
--- a/backend/templates/stream.conf
+++ b/backend/templates/stream.conf
@@ -5,12 +5,10 @@
 {% if enabled %}
 {% if tcp_forwarding == 1 or tcp_forwarding == true -%}
 server {
-  listen {{ incoming_port }};
-{% if ipv6 -%}
-  listen [::]:{{ incoming_port }};
-{% else -%}
-  #listen [::]:{{ incoming_port }};
-{% endif %}
+  listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
+  {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
+
+  {%- include "_certificates_stream.conf" %}
 
   proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
 
@@ -19,14 +17,12 @@ server {
   include /data/nginx/custom/server_stream_tcp[.]conf;
 }
 {% endif %}
-{% if udp_forwarding == 1 or udp_forwarding == true %}
+
+{% if udp_forwarding == 1 or udp_forwarding == true -%}
 server {
   listen {{ incoming_port }} udp;
-{% if ipv6 -%}
-  listen [::]:{{ incoming_port }} udp;
-{% else -%}
-  #listen [::]:{{ incoming_port }} udp;
-{% endif %}
+  {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
+
   proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
 
   # Custom
diff --git a/backend/validate-schema.js b/backend/validate-schema.js
new file mode 100644
index 000000000..71a05c818
--- /dev/null
+++ b/backend/validate-schema.js
@@ -0,0 +1,16 @@
+const SwaggerParser = require('@apidevtools/swagger-parser');
+const chalk         = require('chalk');
+const schema        = require('./schema');
+const log           = console.log;
+
+schema.getCompiledSchema().then(async (swaggerJSON) => {
+	try {
+		const api = await SwaggerParser.validate(swaggerJSON);
+		console.log('API name: %s, Version: %s', api.info.title, api.info.version);
+		log(chalk.green('❯ Schema is valid'));
+	} catch (e) {
+		console.error(e);
+		log(chalk.red('❯', e.message), '\n');
+		process.exit(1);
+	}
+});
diff --git a/backend/yarn.lock b/backend/yarn.lock
index af2095496..cea8210bc 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -2,15 +2,47 @@
 # yarn lockfile v1
 
 
-"@apidevtools/json-schema-ref-parser@8.0.0":
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#9eb749499b3f8d919e90bb141e4b6f67aee4692d"
-  integrity sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==
+"@apidevtools/json-schema-ref-parser@9.0.6":
+  version "9.0.6"
+  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c"
+  integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==
   dependencies:
-    "@jsdevtools/ono" "^7.1.0"
+    "@jsdevtools/ono" "^7.1.3"
     call-me-maybe "^1.0.1"
     js-yaml "^3.13.1"
 
+"@apidevtools/json-schema-ref-parser@^11.7.0":
+  version "11.7.0"
+  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz#228d72018a0e7cbee744b677eaa01a8968f302d9"
+  integrity sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==
+  dependencies:
+    "@jsdevtools/ono" "^7.1.3"
+    "@types/json-schema" "^7.0.15"
+    js-yaml "^4.1.0"
+
+"@apidevtools/openapi-schemas@^2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17"
+  integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==
+
+"@apidevtools/swagger-methods@^3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267"
+  integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==
+
+"@apidevtools/swagger-parser@^10.1.0":
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz#a987d71e5be61feb623203be0c96e5985b192ab6"
+  integrity sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==
+  dependencies:
+    "@apidevtools/json-schema-ref-parser" "9.0.6"
+    "@apidevtools/openapi-schemas" "^2.1.0"
+    "@apidevtools/swagger-methods" "^3.0.2"
+    "@jsdevtools/ono" "^7.1.3"
+    ajv "^8.6.3"
+    ajv-draft-04 "^1.0.0"
+    call-me-maybe "^1.0.1"
+
 "@eslint-community/eslint-utils@^4.2.0":
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a"
@@ -67,7 +99,7 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
-"@jsdevtools/ono@^7.1.0":
+"@jsdevtools/ono@^7.1.3":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
   integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
@@ -146,6 +178,11 @@
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
 
+"@types/json-schema@^7.0.15":
+  version "7.0.15"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+  integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
 abbrev@1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -193,7 +230,12 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"
 
-ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4:
+ajv-draft-04@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8"
+  integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==
+
+ajv@^6.10.0, ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -203,6 +245,16 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^8.17.1, ajv@^8.6.3:
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+  integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
 ajv@^8.6.2:
   version "8.12.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
@@ -360,6 +412,11 @@ async@^3.2.0:
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
   integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
 
+aws-ssl-profiles@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641"
+  integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
+
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -383,11 +440,6 @@ bcrypt@^5.0.0:
     node-addon-api "^3.0.0"
     node-pre-gyp "0.15.0"
 
-bignumber.js@9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
-  integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
-
 binary-extensions@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
@@ -407,10 +459,10 @@ blueimp-md5@^2.16.0:
   resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.17.0.tgz#f4fcac088b115f7b4045f19f5da59e9d01b1bb96"
   integrity sha512-x5PKJHY5rHQYaADj6NwPUR2QRCUVSggPzrUKkeENpj871o9l9IefJbO2jkT5UvYykeOK9dx0VmkIo6dZ+vThYw==
 
-body-parser@1.20.2, body-parser@^1.19.0:
-  version "1.20.2"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
-  integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
+body-parser@1.20.3, body-parser@^1.20.3:
+  version "1.20.3"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
+  integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
   dependencies:
     bytes "3.1.2"
     content-type "~1.0.5"
@@ -420,7 +472,7 @@ body-parser@1.20.2, body-parser@^1.19.0:
     http-errors "2.0.0"
     iconv-lite "0.4.24"
     on-finished "2.4.1"
-    qs "6.11.0"
+    qs "6.13.0"
     raw-body "2.5.2"
     type-is "~1.6.18"
     unpipe "1.0.0"
@@ -552,6 +604,14 @@ camelcase@^5.0.0, camelcase@^5.3.1:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
+chalk@4.1.2, chalk@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
 chalk@^2.3.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -569,14 +629,6 @@ chalk@^3.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chalk@^4.0.0:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
-  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
 chokidar@^3.2.2:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1"
@@ -778,9 +830,9 @@ crc32-stream@^4.0.2:
     readable-stream "^3.4.0"
 
 cross-spawn@^7.0.2:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
-  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"
@@ -858,6 +910,11 @@ delegates@^1.0.0:
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
   integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
 
+denque@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+  integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
+
 depd@2.0.0, depd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -936,6 +993,11 @@ encodeurl@~1.0.2:
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
+encodeurl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+  integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+
 encoding@^0.1.12:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
@@ -1133,37 +1195,37 @@ express-fileupload@^1.1.9:
   dependencies:
     busboy "^0.3.1"
 
-express@^4.19.2:
-  version "4.19.2"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
-  integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
+express@^4.20.0:
+  version "4.20.0"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48"
+  integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==
   dependencies:
     accepts "~1.3.8"
     array-flatten "1.1.1"
-    body-parser "1.20.2"
+    body-parser "1.20.3"
     content-disposition "0.5.4"
     content-type "~1.0.4"
     cookie "0.6.0"
     cookie-signature "1.0.6"
     debug "2.6.9"
     depd "2.0.0"
-    encodeurl "~1.0.2"
+    encodeurl "~2.0.0"
     escape-html "~1.0.3"
     etag "~1.8.1"
     finalhandler "1.2.0"
     fresh "0.5.2"
     http-errors "2.0.0"
-    merge-descriptors "1.0.1"
+    merge-descriptors "1.0.3"
     methods "~1.1.2"
     on-finished "2.4.1"
     parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
+    path-to-regexp "0.1.10"
     proxy-addr "~2.0.7"
     qs "6.11.0"
     range-parser "~1.2.1"
     safe-buffer "5.2.1"
-    send "0.18.0"
-    serve-static "1.15.0"
+    send "0.19.0"
+    serve-static "1.16.0"
     setprototypeof "1.2.0"
     statuses "2.0.1"
     type-is "~1.6.18"
@@ -1185,6 +1247,11 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
+fast-uri@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024"
+  integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==
+
 fastq@^1.6.0:
   version "1.15.0"
   resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
@@ -1354,6 +1421,13 @@ gauge@~2.7.3:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
+generate-function@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
+  integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
+  dependencies:
+    is-property "^1.0.2"
+
 get-caller-file@^2.0.1:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@@ -1598,7 +1672,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-iconv-lite@^0.6.2:
+iconv-lite@^0.6.2, iconv-lite@^0.6.3:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
   integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -1801,6 +1875,11 @@ is-path-inside@^3.0.3:
   resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
   integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
 
+is-property@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+  integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
+
 is-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
@@ -1856,13 +1935,6 @@ json-parse-better-errors@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
-json-schema-ref-parser@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#7c758fac2cf822c05e837abd0a13f8fa2c15ffd4"
-  integrity sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ==
-  dependencies:
-    "@apidevtools/json-schema-ref-parser" "8.0.0"
-
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -2028,6 +2100,11 @@ lodash@^4.17.21:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
+long@^5.2.1:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
+  integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
+
 lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@@ -2045,6 +2122,16 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
+lru-cache@^7.14.1:
+  version "7.18.3"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+  integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
+
+lru-cache@^8.0.0:
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
+  integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
+
 make-dir@^3.0.0, make-dir@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -2079,10 +2166,10 @@ media-typer@0.3.0:
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
-  integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+merge-descriptors@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
+  integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
 
 methods@~1.1.2:
   version "1.1.2"
@@ -2248,15 +2335,27 @@ ms@2.1.3, ms@^2.0.0:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-mysql@^2.18.1:
-  version "2.18.1"
-  resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
-  integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==
+mysql2@^3.11.1:
+  version "3.11.1"
+  resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.11.1.tgz#edfb856e2176fcf43d2cc066dd4959e9fc76ea85"
+  integrity sha512-Oc8Zffd0gpIJnJ/NOMp6IiiJJDdWc7nmWpS+UE3K9feTpYia8XkbgL6EaOJYz52f6+2pAoC0eAQqUzal4lnNGQ==
+  dependencies:
+    aws-ssl-profiles "^1.1.1"
+    denque "^2.1.0"
+    generate-function "^2.3.1"
+    iconv-lite "^0.6.3"
+    long "^5.2.1"
+    lru-cache "^8.0.0"
+    named-placeholders "^1.1.3"
+    seq-queue "^0.0.5"
+    sqlstring "^2.3.2"
+
+named-placeholders@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
+  integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
   dependencies:
-    bignumber.js "9.0.0"
-    readable-stream "2.3.7"
-    safe-buffer "5.1.2"
-    sqlstring "2.3.1"
+    lru-cache "^7.14.1"
 
 natural-compare@^1.4.0:
   version "1.4.0"
@@ -2623,10 +2722,10 @@ path-parse@^1.0.7:
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
-  integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-to-regexp@0.1.10:
+  version "0.1.10"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
+  integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
 
 path@^0.12.7:
   version "0.12.7"
@@ -2636,11 +2735,67 @@ path@^0.12.7:
     process "^0.11.1"
     util "^0.10.3"
 
+pg-cloudflare@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
+  integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==
+
 pg-connection-string@2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
   integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
 
+pg-connection-string@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37"
+  integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==
+
+pg-int8@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
+  integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
+
+pg-pool@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec"
+  integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==
+
+pg-protocol@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93"
+  integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==
+
+pg-types@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
+  integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
+  dependencies:
+    pg-int8 "1.0.1"
+    postgres-array "~2.0.0"
+    postgres-bytea "~1.0.0"
+    postgres-date "~1.0.4"
+    postgres-interval "^1.1.0"
+
+pg@^8.13.1:
+  version "8.13.1"
+  resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080"
+  integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==
+  dependencies:
+    pg-connection-string "^2.7.0"
+    pg-pool "^3.7.0"
+    pg-protocol "^1.7.0"
+    pg-types "^2.1.0"
+    pgpass "1.x"
+  optionalDependencies:
+    pg-cloudflare "^1.1.1"
+
+pgpass@1.x:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d"
+  integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==
+  dependencies:
+    split2 "^4.1.0"
+
 picomatch@^2.0.4, picomatch@^2.2.1:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
@@ -2659,6 +2814,28 @@ pkg-conf@^2.1.0:
     find-up "^2.0.0"
     load-json-file "^4.0.0"
 
+postgres-array@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
+  integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
+
+postgres-bytea@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
+  integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==
+
+postgres-date@~1.0.4:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
+  integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
+
+postgres-interval@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
+  integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
+  dependencies:
+    xtend "^4.0.0"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -2742,6 +2919,13 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
+qs@6.13.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+  integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+  dependencies:
+    side-channel "^1.0.6"
+
 querystring@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
@@ -2777,7 +2961,7 @@ rc@^1.2.7, rc@^1.2.8:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-readable-stream@2.3.7, readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6:
+readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -2969,10 +3153,34 @@ send@0.18.0:
     range-parser "~1.2.1"
     statuses "2.0.1"
 
-serve-static@1.15.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
-  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+send@0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
+  integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
+  dependencies:
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    mime "1.6.0"
+    ms "2.1.3"
+    on-finished "2.4.1"
+    range-parser "~1.2.1"
+    statuses "2.0.1"
+
+seq-queue@^0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
+  integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
+
+serve-static@1.16.0:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92"
+  integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==
   dependencies:
     encodeurl "~1.0.2"
     escape-html "~1.0.3"
@@ -3013,7 +3221,7 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-side-channel@^1.0.4:
+side-channel@^1.0.4, side-channel@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
   integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
@@ -3064,6 +3272,11 @@ socks@^2.6.2:
     ip "^2.0.0"
     smart-buffer "^4.2.0"
 
+split2@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
+  integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -3080,10 +3293,10 @@ sqlite3@5.1.6:
   optionalDependencies:
     node-gyp "8.x"
 
-sqlstring@2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
-  integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=
+sqlstring@^2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c"
+  integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==
 
 ssri@^8.0.0, ssri@^8.0.1:
   version "8.0.1"
@@ -3535,6 +3748,11 @@ xdg-basedir@^4.0.0:
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
   integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
 
+xtend@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
 y18n@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 799ee2a63..0603e2ded 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -3,6 +3,8 @@
 
 # This file assumes that the frontend has been built using ./scripts/frontend-build
 
+FROM nginxproxymanager/testca AS testca
+FROM letsencrypt/pebble AS pebbleca
 FROM nginxproxymanager/nginx-full:certbot-node
 
 ARG TARGETPLATFORM
@@ -45,6 +47,8 @@ RUN yarn install \
 
 # add late to limit cache-busting by modifications
 COPY docker/rootfs /
+COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
+COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
 
 # Remove frontend service not required for prod, dev nginx config as well
 RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \
diff --git a/docker/ci.env b/docker/ci.env
new file mode 100644
index 000000000..7128295dd
--- /dev/null
+++ b/docker/ci.env
@@ -0,0 +1,8 @@
+AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
+AUTHENTIK_REDIS__HOST=authentik-redis
+AUTHENTIK_POSTGRESQL__HOST=db-postgres
+AUTHENTIK_POSTGRESQL__USER=authentik
+AUTHENTIK_POSTGRESQL__NAME=authentik
+AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
+AUTHENTIK_BOOTSTRAP_PASSWORD=admin
+AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com
diff --git a/docker/ci/postgres/authentik.sql.gz b/docker/ci/postgres/authentik.sql.gz
new file mode 100644
index 000000000..49665d4e6
Binary files /dev/null and b/docker/ci/postgres/authentik.sql.gz differ
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 3c21849cd..dcb1f1f9b 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -1,7 +1,10 @@
+FROM nginxproxymanager/testca AS testca
+FROM letsencrypt/pebble AS pebbleca
 FROM nginxproxymanager/nginx-full:certbot-node
 LABEL maintainer="Jamie Curnow <jc@jc21.com>"
 
-# See: https://github.com/just-containers/s6-overlay/blob/master/README.md
+SHELL ["/bin/bash", "-o", "pipefail", "-c"]
+
 ENV SUPPRESS_NO_CONFIG_WARNING=1 \
 	S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
 	S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
@@ -17,17 +20,21 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
 	&& rm -rf /var/lib/apt/lists/*
 
 # Task
-RUN cd /usr \
-	&& curl -sL https://taskfile.dev/install.sh | sh \
-	&& cd /root
+WORKDIR /usr
+RUN curl -sL https://taskfile.dev/install.sh | sh
+WORKDIR /root
 
 COPY rootfs /
-RUN rm -f /etc/nginx/conf.d/production.conf
-RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager
-
-# s6 overlay
 COPY scripts/install-s6 /tmp/install-s6
-RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
+RUN rm -f /etc/nginx/conf.d/production.conf \
+	&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \
+	&& /tmp/install-s6 "${TARGETPLATFORM}" \
+	&& rm -f /tmp/install-s6 \
+	&& chmod 644 -R /root/.cache
+
+# Certs for testing purposes
+COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
+COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
 
 EXPOSE 80 81 443
 ENTRYPOINT [ "/init" ]
diff --git a/docker/dev/squid.conf b/docker/dev/squid.conf
new file mode 100644
index 000000000..cdd749f58
--- /dev/null
+++ b/docker/dev/squid.conf
@@ -0,0 +1,92 @@
+#	WELCOME TO SQUID 6.6
+#	----------------------------
+#
+#	This is the documentation for the Squid configuration file.
+#	This documentation can also be found online at:
+#		http://www.squid-cache.org/Doc/config/
+#
+#	You may wish to look at the Squid home page and wiki for the
+#	FAQ and other documentation:
+#		http://www.squid-cache.org/
+#		https://wiki.squid-cache.org/SquidFaq
+#		https://wiki.squid-cache.org/ConfigExamples
+#
+
+# Example rule allowing access from your local networks.
+# Adapt to list your (internal) IP networks from where browsing
+# should be allowed
+acl localnet src 0.0.0.1-0.255.255.255	# RFC 1122 "this" network (LAN)
+acl localnet src 10.0.0.0/8		# RFC 1918 local private network (LAN)
+acl localnet src 100.64.0.0/10		# RFC 6598 shared address space (CGN)
+acl localnet src 169.254.0.0/16 	# RFC 3927 link-local (directly plugged) machines
+acl localnet src 172.0.0.0/8
+acl localnet src 192.168.0.0/16		# RFC 1918 local private network (LAN)
+acl localnet src fc00::/7       	# RFC 4193 local private network range
+acl localnet src fe80::/10      	# RFC 4291 link-local (directly plugged) machines
+
+acl SSL_ports port 443
+acl Safe_ports port 80		# http
+acl Safe_ports port 81
+acl Safe_ports port 443		# https
+
+#
+# Recommended minimum Access Permission configuration:
+#
+# Deny requests to certain unsafe ports
+http_access deny !Safe_ports
+
+# Deny CONNECT to other than secure SSL ports
+http_access deny CONNECT !SSL_ports
+
+# Only allow cachemgr access from localhost
+http_access allow localhost manager
+http_access deny manager
+
+# This default configuration only allows localhost requests because a more
+# permissive Squid installation could introduce new attack vectors into the
+# network by proxying external TCP connections to unprotected services.
+http_access allow localhost
+
+# The two deny rules below are unnecessary in this default configuration
+# because they are followed by a "deny all" rule. However, they may become
+# critically important when you start allowing external requests below them.
+
+# Protect web applications running on the same server as Squid. They often
+# assume that only local users can access them at "localhost" ports.
+http_access deny to_localhost
+
+# Protect cloud servers that provide local users with sensitive info about
+# their server via certain well-known link-local (a.k.a. APIPA) addresses.
+http_access deny to_linklocal
+
+#
+# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
+#
+include /etc/squid/conf.d/*.conf
+
+# For example, to allow access from your local networks, you may uncomment the
+# following rule (and/or add rules that match your definition of "local"):
+# http_access allow localnet
+
+# And finally deny all other access to this proxy
+http_access deny all
+
+# Squid normally listens to port 3128
+http_port 3128
+
+# Leave coredumps in the first cache dir
+coredump_dir /var/spool/squid
+
+#
+# Add any of your own refresh_pattern entries above these.
+#
+refresh_pattern ^ftp:		1440	20%	10080
+refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
+refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
+refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
+refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
+refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
+# example pattern for deb packages
+#refresh_pattern (\.deb|\.udeb)$   129600 100% 129600
+refresh_pattern .		0	20%	4320
+
diff --git a/docker/docker-compose.ci.mysql.yml b/docker/docker-compose.ci.mysql.yml
index 388cdb382..108a1dca3 100644
--- a/docker/docker-compose.ci.mysql.yml
+++ b/docker/docker-compose.ci.mysql.yml
@@ -18,6 +18,7 @@ services:
       MYSQL_DATABASE: 'npm'
       MYSQL_USER: 'npm'
       MYSQL_PASSWORD: 'npmpass'
+      MARIADB_AUTO_UPGRADE: '1'
     volumes:
       - mysql_vol:/var/lib/mysql
     networks:
diff --git a/docker/docker-compose.ci.postgres.yml b/docker/docker-compose.ci.postgres.yml
new file mode 100644
index 000000000..c4468c68b
--- /dev/null
+++ b/docker/docker-compose.ci.postgres.yml
@@ -0,0 +1,78 @@
+# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
+services:
+
+  cypress:
+    environment:
+      CYPRESS_stack: 'postgres'
+
+  fullstack:
+    environment:
+      DB_POSTGRES_HOST: 'db-postgres'
+      DB_POSTGRES_PORT: '5432'
+      DB_POSTGRES_USER: 'npm'
+      DB_POSTGRES_PASSWORD: 'npmpass'
+      DB_POSTGRES_NAME: 'npm'
+    depends_on:
+      - db-postgres
+      - authentik
+      - authentik-worker
+      - authentik-ldap
+
+  db-postgres:
+    image: postgres:latest
+    environment:
+      POSTGRES_USER: 'npm'
+      POSTGRES_PASSWORD: 'npmpass'
+      POSTGRES_DB: 'npm'
+    volumes:
+      - psql_vol:/var/lib/postgresql/data
+      - ./ci/postgres:/docker-entrypoint-initdb.d
+    networks:
+      - fulltest
+
+  authentik-redis:
+    image: 'redis:alpine'
+    command: --save 60 1 --loglevel warning
+    restart: unless-stopped
+    healthcheck:
+      test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
+      start_period: 20s
+      interval: 30s
+      retries: 5
+      timeout: 3s
+    volumes:
+      - redis_vol:/data
+
+  authentik:
+    image: ghcr.io/goauthentik/server:2024.10.1
+    restart: unless-stopped
+    command: server
+    env_file:
+      - ci.env
+    depends_on:
+      - authentik-redis
+      - db-postgres
+
+  authentik-worker:
+    image: ghcr.io/goauthentik/server:2024.10.1
+    restart: unless-stopped
+    command: worker
+    env_file:
+      - ci.env
+    depends_on:
+      - authentik-redis
+      - db-postgres
+
+  authentik-ldap:
+    image: ghcr.io/goauthentik/ldap:2024.10.1
+    environment:
+      AUTHENTIK_HOST: 'http://authentik:9000'
+      AUTHENTIK_INSECURE: 'true'
+      AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp'
+    restart: unless-stopped
+    depends_on:
+      - authentik
+
+volumes:
+  psql_vol:
+  redis_vol:
diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml
index b4a448063..280a05465 100644
--- a/docker/docker-compose.ci.yml
+++ b/docker/docker-compose.ci.yml
@@ -9,6 +9,9 @@ services:
     environment:
       DEBUG: 'true'
       FORCE_COLOR: 1
+      # Required for DNS Certificate provisioning in CI
+      LE_SERVER: 'https://ca.internal/acme/acme/directory'
+      REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
     volumes:
       - 'npm_data_ci:/data'
       - 'npm_le_ci:/etc/letsencrypt'
@@ -19,6 +22,10 @@ services:
       test: ["CMD", "/usr/bin/check-health"]
       interval: 10s
       timeout: 3s
+    expose:
+      - '80-81/tcp'
+      - '443/tcp'
+      - '1500-1503/tcp'
     networks:
       fulltest:
         aliases:
@@ -37,7 +44,7 @@ services:
           - ca.internal
 
   pdns:
-    image: pschiffe/pdns-mysql
+    image: pschiffe/pdns-mysql:4.8
     volumes:
       - '/etc/localtime:/etc/localtime:ro'
     environment:
@@ -91,14 +98,25 @@ services:
       context: ../
       dockerfile: test/cypress/Dockerfile
     environment:
-      CYPRESS_baseUrl: 'http://fullstack:81'
+      HTTP_PROXY: 'squid:3128'
+      HTTPS_PROXY: 'squid:3128'
     volumes:
-      - 'cypress_logs:/results'
+      - 'cypress_logs:/test/results'
       - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - '/etc/localtime:/etc/localtime:ro'
     command: cypress run --browser chrome --config-file=cypress/config/ci.js
     networks:
       - fulltest
 
+  squid:
+    image: ubuntu/squid
+    volumes:
+      - './dev/squid.conf:/etc/squid/squid.conf:ro'
+      - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - '/etc/localtime:/etc/localtime:ro'
+    networks:
+      - fulltest
+
 volumes:
   cypress_logs:
   npm_data_ci:
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 14ca2f7a7..5abe057b0 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -1,9 +1,9 @@
 # WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
 services:
 
-  npm:
-    image: nginxproxymanager:dev
-    container_name: npm_core
+  fullstack:
+    image: npm2dev:core
+    container_name: npm2dev.core
     build:
       context: ./
       dockerfile: ./dev/Dockerfile
@@ -12,7 +12,11 @@ services:
       - 3081:81
       - 3443:443
     networks:
-      - nginx_proxy_manager
+      nginx_proxy_manager:
+        aliases:
+          - website1.example.com
+          - website2.example.com
+          - website3.example.com
     environment:
       PUID: 1000
       PGID: 1000
@@ -22,26 +26,44 @@ services:
       DEVELOPMENT: 'true'
       LE_STAGING: 'true'
       # db:
-      DB_MYSQL_HOST: 'db'
-      DB_MYSQL_PORT: '3306'
-      DB_MYSQL_USER: 'npm'
-      DB_MYSQL_PASSWORD: 'npm'
-      DB_MYSQL_NAME: 'npm'
+      # DB_MYSQL_HOST: 'db'
+      # DB_MYSQL_PORT: '3306'
+      # DB_MYSQL_USER: 'npm'
+      # DB_MYSQL_PASSWORD: 'npm'
+      # DB_MYSQL_NAME: 'npm'
+      # db-postgres:
+      DB_POSTGRES_HOST: 'db-postgres'
+      DB_POSTGRES_PORT: '5432'
+      DB_POSTGRES_USER: 'npm'
+      DB_POSTGRES_PASSWORD: 'npmpass'
+      DB_POSTGRES_NAME: 'npm'
       # DB_SQLITE_FILE: "/data/database.sqlite"
       # DISABLE_IPV6: "true"
+      # Required for DNS Certificate provisioning testing:
+      LE_SERVER: 'https://ca.internal/acme/acme/directory'
+      REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
     volumes:
       - npm_data:/data
       - le_data:/etc/letsencrypt
+      - './dev/resolv.conf:/etc/resolv.conf:ro'
       - ../backend:/app
       - ../frontend:/app/frontend
       - ../global:/app/global
+    healthcheck:
+      test: ["CMD", "/usr/bin/check-health"]
+      interval: 10s
+      timeout: 3s
     depends_on:
       - db
+      - db-postgres
+      - authentik
+      - authentik-worker
+      - authentik-ldap
     working_dir: /app
 
   db:
     image: jc21/mariadb-aria
-    container_name: npm_db
+    container_name: npm2dev.db
     ports:
       - 33306:3306
     networks:
@@ -54,14 +76,193 @@ services:
     volumes:
       - db_data:/var/lib/mysql
 
+  db-postgres:
+    image: postgres:latest
+    container_name: npm2dev.db-postgres
+    networks:
+      - nginx_proxy_manager
+    environment:
+      POSTGRES_USER: 'npm'
+      POSTGRES_PASSWORD: 'npmpass'
+      POSTGRES_DB: 'npm'
+    volumes:
+      - psql_data:/var/lib/postgresql/data
+      - ./ci/postgres:/docker-entrypoint-initdb.d
+
+  stepca:
+    image: jc21/testca
+    container_name: npm2dev.stepca
+    volumes:
+      - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - '/etc/localtime:/etc/localtime:ro'
+    networks:
+      nginx_proxy_manager:
+        aliases:
+          - ca.internal
+
+  dnsrouter:
+    image: jc21/dnsrouter
+    container_name: npm2dev.dnsrouter
+    volumes:
+      - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
+    networks:
+      - nginx_proxy_manager
+
+  swagger:
+    image: swaggerapi/swagger-ui:latest
+    container_name: npm2dev.swagger
+    ports:
+      - 3082:80
+    environment:
+      URL: "http://npm:81/api/schema"
+      PORT: '80'
+    depends_on:
+      - fullstack
+
+  squid:
+    image: ubuntu/squid
+    container_name: npm2dev.squid
+    volumes:
+      - './dev/squid.conf:/etc/squid/squid.conf:ro'
+      - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - '/etc/localtime:/etc/localtime:ro'
+    networks:
+      - nginx_proxy_manager
+    ports:
+      - 8128:3128
+
+  pdns:
+    image: pschiffe/pdns-mysql:4.8
+    container_name: npm2dev.pdns
+    volumes:
+      - '/etc/localtime:/etc/localtime:ro'
+    environment:
+      PDNS_master: 'yes'
+      PDNS_api: 'yes'
+      PDNS_api_key: 'npm'
+      PDNS_webserver: 'yes'
+      PDNS_webserver_address: '0.0.0.0'
+      PDNS_webserver_password: 'npm'
+      PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
+      PDNS_version_string: 'anonymous'
+      PDNS_default_ttl: 1500
+      PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
+      PDNS_gmysql_host: pdns-db
+      PDNS_gmysql_port: 3306
+      PDNS_gmysql_user: pdns
+      PDNS_gmysql_password: pdns
+      PDNS_gmysql_dbname: pdns
+    depends_on:
+      - pdns-db
+    networks:
+      nginx_proxy_manager:
+        aliases:
+          - ns1.pdns
+          - ns2.pdns
+
+  pdns-db:
+    image: mariadb
+    container_name: npm2dev.pdns-db
+    environment:
+      MYSQL_ROOT_PASSWORD: 'pdns'
+      MYSQL_DATABASE: 'pdns'
+      MYSQL_USER: 'pdns'
+      MYSQL_PASSWORD: 'pdns'
+    volumes:
+      - 'pdns_mysql:/var/lib/mysql'
+      - '/etc/localtime:/etc/localtime:ro'
+      - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
+    networks:
+      - nginx_proxy_manager
+
+  cypress:
+    image: npm2dev:cypress
+    container_name: npm2dev.cypress
+    build:
+      context: ../
+      dockerfile: test/cypress/Dockerfile
+    environment:
+      HTTP_PROXY: 'squid:3128'
+      HTTPS_PROXY: 'squid:3128'
+    volumes:
+      - '../test/results:/results'
+      - './dev/resolv.conf:/etc/resolv.conf:ro'
+      - '/etc/localtime:/etc/localtime:ro'
+    command: cypress run --browser chrome --config-file=cypress/config/ci.js
+    networks:
+      - nginx_proxy_manager
+
+  authentik-redis:
+    image: 'redis:alpine'
+    container_name: npm2dev.authentik-redis
+    command: --save 60 1 --loglevel warning
+    networks:
+      - nginx_proxy_manager
+    restart: unless-stopped
+    healthcheck:
+      test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
+      start_period: 20s
+      interval: 30s
+      retries: 5
+      timeout: 3s
+    volumes:
+      - redis_data:/data
+
+  authentik:
+    image: ghcr.io/goauthentik/server:2024.10.1
+    container_name: npm2dev.authentik
+    restart: unless-stopped
+    command: server
+    networks:
+      - nginx_proxy_manager
+    env_file:
+      - ci.env
+    ports:
+      - 9000:9000
+    depends_on:
+      - authentik-redis
+      - db-postgres
+
+  authentik-worker:
+    image: ghcr.io/goauthentik/server:2024.10.1
+    container_name: npm2dev.authentik-worker
+    restart: unless-stopped
+    command: worker
+    networks:
+      - nginx_proxy_manager
+    env_file:
+      - ci.env
+    depends_on:
+      - authentik-redis
+      - db-postgres
+
+  authentik-ldap:
+    image: ghcr.io/goauthentik/ldap:2024.10.1
+    container_name: npm2dev.authentik-ldap
+    networks:
+      - nginx_proxy_manager
+    environment:
+      AUTHENTIK_HOST: 'http://authentik:9000'
+      AUTHENTIK_INSECURE: 'true'
+      AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp'
+    restart: unless-stopped
+    depends_on:
+      - authentik
+
 volumes:
   npm_data:
-    name: npm_core_data
+    name: npm2dev_core_data
   le_data:
-    name: npm_le_data
+    name: npm2dev_le_data
   db_data:
-    name: npm_db_data
+    name: npm2dev_db_data
+  pdns_mysql:
+    name: npnpm2dev_pdns_mysql
+  psql_data:
+    name: npm2dev_psql_data
+  redis_data:
+    name: npm2dev_redis_data
 
 networks:
   nginx_proxy_manager:
-    name: npm_network
+    name: npm2dev_network
diff --git a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager
index 275b0aa6a..de9772971 100644
--- a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager
+++ b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager
@@ -8,7 +8,7 @@
     compress
     sharedscripts
     postrotate
-    /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
+    kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true
     endscript
 }
 
@@ -22,6 +22,6 @@
     compress
     sharedscripts
     postrotate
-    /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
+    kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true
     endscript
-}
\ No newline at end of file
+}
diff --git a/docker/rootfs/etc/nginx/conf.d/include/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf
index b7037343d..5a90beb8a 100644
--- a/docker/rootfs/etc/nginx/conf.d/include/assets.conf
+++ b/docker/rootfs/etc/nginx/conf.d/include/assets.conf
@@ -1,4 +1,4 @@
-location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\.map|js\.map)$ {
+location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ {
 	if_modified_since off;
 
 	# use the public cache
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf
new file mode 100644
index 000000000..433555dfa
--- /dev/null
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf
@@ -0,0 +1,2 @@
+ssl_session_timeout 5m;
+ssl_session_cache shared:SSL_stream:50m;
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
new file mode 100644
index 000000000..aa7ba2cb7
--- /dev/null
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
@@ -0,0 +1,2 @@
+ssl_session_timeout 5m;
+ssl_session_cache shared:SSL:50m;
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
index 233abb6e9..b5dacfb57 100644
--- a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
@@ -1,6 +1,3 @@
-ssl_session_timeout 5m;
-ssl_session_cache shared:SSL:50m;
-
 # intermediate configuration. tweak to your needs.
 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
diff --git a/docker/rootfs/var/www/html/index.html b/docker/rootfs/var/www/html/index.html
index 8478b47f1..703db883d 100644
--- a/docker/rootfs/var/www/html/index.html
+++ b/docker/rootfs/var/www/html/index.html
@@ -5,7 +5,7 @@
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
         <meta name="viewport" content="width=device-width, initial-scale=1">
         <title>Default Site</title>
-        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
+        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
         <style>
             .jumbotron { margin-top: 50px; }
         </style>
diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6
index 2922735b2..5f3b73ec5 100755
--- a/docker/scripts/install-s6
+++ b/docker/scripts/install-s6
@@ -8,7 +8,7 @@ BLUE='\E[1;34m'
 GREEN='\E[1;32m'
 RESET='\E[0m'
 
-S6_OVERLAY_VERSION=3.1.5.0
+S6_OVERLAY_VERSION=3.2.0.2
 TARGETPLATFORM=${1:-linux/amd64}
 
 # Determine the correct binary file for the architecture given
diff --git a/docs/package.json b/docs/package.json
index cc3d08c1c..3e3dcba25 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,7 +5,7 @@
     "preview": "vitepress preview"
   },
   "devDependencies": {
-    "vitepress": "^1.1.4"
+    "vitepress": "^1.4.0"
   },
   "dependencies": {}
 }
diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md
index efeaefec3..373fd08bb 100644
--- a/docs/src/advanced-config/index.md
+++ b/docs/src/advanced-config/index.md
@@ -50,7 +50,6 @@ networks:
 Let's look at a Portainer example:
 
 ```yml
-version: '3.8'
 services:
 
   portainer:
@@ -92,8 +91,6 @@ This image supports the use of Docker secrets to import from files and keep sens
 You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name.
 
 ```yml
-version: '3.8'
-
 secrets:
   # Secrets are single-line text files where the sole content is the secret
   # Paths in this example assume that secrets are kept in local folder called ".secrets"
@@ -184,6 +181,7 @@ You can add your custom configuration snippet files at `/data/nginx/custom` as f
  - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block
  - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block
  - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block
+ - `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block
 
 Every file is optional.
 
diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md
index ad350b3b9..fcf176fa9 100644
--- a/docs/src/guide/index.md
+++ b/docs/src/guide/index.md
@@ -62,7 +62,6 @@ I won't go in to too much detail here but here are the basics for someone new to
 2. Create a docker-compose.yml file similar to this:
 
 ```yml
-version: '3.8'
 services:
   app:
     image: 'jc21/nginx-proxy-manager:latest'
diff --git a/docs/src/setup/index.md b/docs/src/setup/index.md
index 9b1505bed..5e126754f 100644
--- a/docs/src/setup/index.md
+++ b/docs/src/setup/index.md
@@ -9,7 +9,6 @@ outline: deep
 Create a `docker-compose.yml` file:
 
 ```yml
-version: '3.8'
 services:
   app:
     image: 'jc21/nginx-proxy-manager:latest'
@@ -22,8 +21,7 @@ services:
       # Add any other Stream port you want to expose
       # - '21:21' # FTP
 
-    # Uncomment the next line if you uncomment anything in the section
-    # environment:
+    environment:
       # Uncomment this if you want to change the location of
       # the SQLite DB file within the container
       # DB_SQLITE_FILE: "/data/database.sqlite"
@@ -55,7 +53,6 @@ are going to use.
 Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container:
 
 ```yml
-version: '3.8'
 services:
   app:
     image: 'jc21/nginx-proxy-manager:latest'
@@ -101,6 +98,53 @@ Please note, that `DB_MYSQL_*` environment variables will take precedent over `D
 
 :::
 
+## Using Postgres database
+
+Similar to the MySQL server setup:
+
+```yml
+services:
+  app:
+    image: 'jc21/nginx-proxy-manager:latest'
+    restart: unless-stopped
+    ports:
+      # These ports are in format <host-port>:<container-port>
+      - '80:80' # Public HTTP Port
+      - '443:443' # Public HTTPS Port
+      - '81:81' # Admin Web Port
+      # Add any other Stream port you want to expose
+      # - '21:21' # FTP
+    environment:
+      # Postgres parameters:
+      DB_POSTGRES_HOST: 'db'
+      DB_POSTGRES_PORT: '5432'
+      DB_POSTGRES_USER: 'npm'
+      DB_POSTGRES_PASSWORD: 'npmpass'
+      DB_POSTGRES_NAME: 'npm'
+      # Uncomment this if IPv6 is not enabled on your host
+      # DISABLE_IPV6: 'true'
+    volumes:
+      - ./data:/data
+      - ./letsencrypt:/etc/letsencrypt
+    depends_on:
+      - db
+
+  db:
+    image: postgres:latest
+    environment:
+      POSTGRES_USER: 'npm'
+      POSTGRES_PASSWORD: 'npmpass'
+      POSTGRES_DB: 'npm'
+    volumes:
+      - ./postgres:/var/lib/postgresql/data
+```
+
+::: warning
+
+Custom Postgres schema is not supported, as such `public` will be used.
+
+:::
+
 ## Running on Raspberry PI / ARM devices
 
 The docker images support the following architectures:
@@ -137,5 +181,13 @@ Email:    admin@example.com
 Password: changeme
 ```
 
-Immediately after logging in with this default user you will be asked to modify your details and change your password.
+Immediately after logging in with this default user you will be asked to modify your details and change your password. You can change defaults with:
+
+
+```
+    environment:
+      INITIAL_ADMIN_EMAIL: my@example.com
+      INITIAL_ADMIN_PASSWORD: mypassword1
+```
+
 
diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md
index c36f54a8f..cd54b45b9 100644
--- a/docs/src/third-party/index.md
+++ b/docs/src/third-party/index.md
@@ -12,6 +12,7 @@ Known integrations:
 - [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager)
 - [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager)
 - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager)
+- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager)
 - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf)
 
 
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 9249476a3..2adc44fd4 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -150,413 +150,498 @@
     "@algolia/logger-common" "4.23.3"
     "@algolia/requester-common" "4.23.3"
 
-"@babel/parser@^7.24.4":
-  version "7.24.5"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790"
-  integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==
-
-"@docsearch/css@3.6.0", "@docsearch/css@^3.6.0":
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb"
-  integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==
-
-"@docsearch/js@^3.6.0":
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.6.0.tgz#f9e46943449b9092d874944f7a80bcc071004cfb"
-  integrity sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==
-  dependencies:
-    "@docsearch/react" "3.6.0"
+"@babel/helper-string-parser@^7.25.7":
+  version "7.25.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54"
+  integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==
+
+"@babel/helper-validator-identifier@^7.25.7":
+  version "7.25.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5"
+  integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==
+
+"@babel/parser@^7.25.3":
+  version "7.25.8"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2"
+  integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==
+  dependencies:
+    "@babel/types" "^7.25.8"
+
+"@babel/types@^7.25.8":
+  version "7.25.8"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1"
+  integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==
+  dependencies:
+    "@babel/helper-string-parser" "^7.25.7"
+    "@babel/helper-validator-identifier" "^7.25.7"
+    to-fast-properties "^2.0.0"
+
+"@docsearch/css@3.6.2", "@docsearch/css@^3.6.2":
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.2.tgz#ccd9c83dbfeaf34efe4e3547ee596714ae7e5891"
+  integrity sha512-vKNZepO2j7MrYBTZIGXvlUOIR+v9KRf70FApRgovWrj3GTs1EITz/Xb0AOlm1xsQBp16clVZj1SY/qaOJbQtZw==
+
+"@docsearch/js@^3.6.2":
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.6.2.tgz#000d7d255e8387e7c5b82c7b87d3060398e1605d"
+  integrity sha512-pS4YZF+VzUogYrkblCucQ0Oy2m8Wggk8Kk7lECmZM60hTbaydSIhJTTiCrmoxtBqV8wxORnOqcqqOfbmkkQEcA==
+  dependencies:
+    "@docsearch/react" "3.6.2"
     preact "^10.0.0"
 
-"@docsearch/react@3.6.0":
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958"
-  integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==
+"@docsearch/react@3.6.2":
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.2.tgz#32b16dd7d5614f0d39e6bc018549816b68d171b8"
+  integrity sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA==
   dependencies:
     "@algolia/autocomplete-core" "1.9.3"
     "@algolia/autocomplete-preset-algolia" "1.9.3"
-    "@docsearch/css" "3.6.0"
+    "@docsearch/css" "3.6.2"
     algoliasearch "^4.19.1"
 
-"@esbuild/aix-ppc64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
-  integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
-
-"@esbuild/android-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
-  integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
-
-"@esbuild/android-arm@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
-  integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
-
-"@esbuild/android-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
-  integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
-
-"@esbuild/darwin-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
-  integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
-
-"@esbuild/darwin-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
-  integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
-
-"@esbuild/freebsd-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
-  integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
-
-"@esbuild/freebsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
-  integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
-
-"@esbuild/linux-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
-  integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
-
-"@esbuild/linux-arm@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
-  integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
-
-"@esbuild/linux-ia32@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
-  integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
-
-"@esbuild/linux-loong64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
-  integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
-
-"@esbuild/linux-mips64el@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
-  integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
-
-"@esbuild/linux-ppc64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
-  integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
-
-"@esbuild/linux-riscv64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
-  integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
-
-"@esbuild/linux-s390x@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
-  integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
-
-"@esbuild/linux-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
-  integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
-
-"@esbuild/netbsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
-  integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
-
-"@esbuild/openbsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
-  integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
-
-"@esbuild/sunos-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
-  integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
-
-"@esbuild/win32-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
-  integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
-
-"@esbuild/win32-ia32@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
-  integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
-
-"@esbuild/win32-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
-  integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
-
-"@jridgewell/sourcemap-codec@^1.4.15":
-  version "1.4.15"
-  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
-  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
-
-"@rollup/rollup-android-arm-eabi@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d"
-  integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==
-
-"@rollup/rollup-android-arm64@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b"
-  integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==
-
-"@rollup/rollup-darwin-arm64@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz#6b66aaf003c70454c292cd5f0236ebdc6ffbdf1a"
-  integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==
-
-"@rollup/rollup-darwin-x64@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b"
-  integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==
-
-"@rollup/rollup-linux-arm-gnueabihf@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14"
-  integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==
-
-"@rollup/rollup-linux-arm-musleabihf@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb"
-  integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==
-
-"@rollup/rollup-linux-arm64-gnu@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a"
-  integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==
-
-"@rollup/rollup-linux-arm64-musl@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af"
-  integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==
-
-"@rollup/rollup-linux-powerpc64le-gnu@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571"
-  integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==
-
-"@rollup/rollup-linux-riscv64-gnu@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f"
-  integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==
-
-"@rollup/rollup-linux-s390x-gnu@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354"
-  integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==
-
-"@rollup/rollup-linux-x64-gnu@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811"
-  integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==
-
-"@rollup/rollup-linux-x64-musl@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385"
-  integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==
-
-"@rollup/rollup-win32-arm64-msvc@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f"
-  integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==
-
-"@rollup/rollup-win32-ia32-msvc@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411"
-  integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==
-
-"@rollup/rollup-win32-x64-msvc@4.17.2":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503"
-  integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==
-
-"@shikijs/core@1.5.0", "@shikijs/core@^1.3.0":
+"@esbuild/aix-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
+  integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
+
+"@esbuild/android-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
+  integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
+
+"@esbuild/android-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
+  integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
+
+"@esbuild/android-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
+  integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
+
+"@esbuild/darwin-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
+  integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@esbuild/darwin-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
+  integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
+
+"@esbuild/freebsd-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
+  integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
+
+"@esbuild/freebsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
+  integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
+
+"@esbuild/linux-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
+  integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
+
+"@esbuild/linux-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
+  integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
+
+"@esbuild/linux-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
+  integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
+
+"@esbuild/linux-loong64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
+  integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
+
+"@esbuild/linux-mips64el@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
+  integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
+
+"@esbuild/linux-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
+  integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
+
+"@esbuild/linux-riscv64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
+  integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
+
+"@esbuild/linux-s390x@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
+  integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
+
+"@esbuild/linux-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
+  integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@esbuild/netbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
+  integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
+
+"@esbuild/openbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
+  integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
+
+"@esbuild/sunos-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
+  integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
+
+"@esbuild/win32-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
+  integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
+
+"@esbuild/win32-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
+  integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
+
+"@esbuild/win32-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
+  integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
+
+"@jridgewell/sourcemap-codec@^1.5.0":
   version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.5.0.tgz#8d594a6d6eb8cfdb9f2de457893e384257f2e0a2"
-  integrity sha512-tdYjQu+jnvlPbJg4OjgCQ16zAfHlLk+RzA9o025aeaIyUww6W/Vd9TQ2t+gdZgK1fox29/L2yyqXLU6ErzYA0w==
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+  integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+
+"@rollup/rollup-android-arm-eabi@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54"
+  integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==
+
+"@rollup/rollup-android-arm64@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e"
+  integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==
+
+"@rollup/rollup-darwin-arm64@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f"
+  integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==
+
+"@rollup/rollup-darwin-x64@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724"
+  integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb"
+  integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==
+
+"@rollup/rollup-linux-arm-musleabihf@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3"
+  integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==
+
+"@rollup/rollup-linux-arm64-gnu@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496"
+  integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==
+
+"@rollup/rollup-linux-arm64-musl@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065"
+  integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d"
+  integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==
+
+"@rollup/rollup-linux-riscv64-gnu@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983"
+  integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==
+
+"@rollup/rollup-linux-s390x-gnu@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58"
+  integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==
+
+"@rollup/rollup-linux-x64-gnu@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b"
+  integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==
+
+"@rollup/rollup-linux-x64-musl@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127"
+  integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==
+
+"@rollup/rollup-win32-arm64-msvc@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5"
+  integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==
+
+"@rollup/rollup-win32-ia32-msvc@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2"
+  integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==
+
+"@rollup/rollup-win32-x64-msvc@4.24.0":
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818"
+  integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==
+
+"@shikijs/core@1.22.0", "@shikijs/core@^1.22.0":
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.0.tgz#74e5d4485e5f7afa85109e322b42e400686f92bb"
+  integrity sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==
+  dependencies:
+    "@shikijs/engine-javascript" "1.22.0"
+    "@shikijs/engine-oniguruma" "1.22.0"
+    "@shikijs/types" "1.22.0"
+    "@shikijs/vscode-textmate" "^9.3.0"
+    "@types/hast" "^3.0.4"
+    hast-util-to-html "^9.0.3"
+
+"@shikijs/engine-javascript@1.22.0":
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz#2e5db29f0421755492f5279f8224ef7a7f907a29"
+  integrity sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==
+  dependencies:
+    "@shikijs/types" "1.22.0"
+    "@shikijs/vscode-textmate" "^9.3.0"
+    oniguruma-to-js "0.4.3"
+
+"@shikijs/engine-oniguruma@1.22.0":
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz#74c661fac4cd1f08f2c09b5d6e2fd2a6720d0401"
+  integrity sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==
+  dependencies:
+    "@shikijs/types" "1.22.0"
+    "@shikijs/vscode-textmate" "^9.3.0"
 
-"@shikijs/transformers@^1.3.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-1.5.0.tgz#7d091152eca97bfddceb26e12234944a217e9b50"
-  integrity sha512-WYCLJ4MhW1LmVqfVjUZny9XLh32kk/mo/y5sCXt5sc5rU21K6LA+yLWHdb0eYhmSc4n+FWTxW3ZNiZs57uwyOA==
+"@shikijs/transformers@^1.22.0":
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-1.22.0.tgz#f36fa4d769e36db9a91e09877cf48b3a04d26aba"
+  integrity sha512-k7iMOYuGQA62KwAuJOQBgH2IQb5vP8uiB3lMvAMGUgAMMurePOx3Z7oNqJdcpxqZP6I9cc7nc4DNqSKduCxmdg==
   dependencies:
-    shiki "1.5.0"
+    shiki "1.22.0"
 
-"@types/estree@1.0.5":
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
-  integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+"@shikijs/types@1.22.0", "@shikijs/types@^1.22.0":
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.0.tgz#d2a572381395c9308b472c8199b8e0289753b9ad"
+  integrity sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==
+  dependencies:
+    "@shikijs/vscode-textmate" "^9.3.0"
+    "@types/hast" "^3.0.4"
+
+"@shikijs/vscode-textmate@^9.3.0":
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab"
+  integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==
+
+"@types/estree@1.0.6":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
+  integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
+
+"@types/hast@^3.0.0", "@types/hast@^3.0.4":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
+  integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
+  dependencies:
+    "@types/unist" "*"
 
 "@types/linkify-it@^5":
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
   integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
 
-"@types/markdown-it@^14.0.1":
-  version "14.1.1"
-  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.1.tgz#06bafb7a4e3f77b62b1f308acf7df76687887e0b"
-  integrity sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==
+"@types/markdown-it@^14.1.2":
+  version "14.1.2"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
+  integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
   dependencies:
     "@types/linkify-it" "^5"
     "@types/mdurl" "^2"
 
+"@types/mdast@^4.0.0":
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
+  integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/mdurl@^2":
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
   integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
 
+"@types/unist@*", "@types/unist@^3.0.0":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
+  integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
+
 "@types/web-bluetooth@^0.0.20":
   version "0.0.20"
   resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
   integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
 
-"@vitejs/plugin-vue@^5.0.4":
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37"
-  integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==
-
-"@vue/compiler-core@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.27.tgz#e69060f4b61429fe57976aa5872cfa21389e4d91"
-  integrity sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==
+"@ungap/structured-clone@^1.0.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
+"@vitejs/plugin-vue@^5.1.4":
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz#72b8b705cfce36b00b59af196195146e356500c4"
+  integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
+
+"@vue/compiler-core@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.11.tgz#3dcd0c1bab10732f44ab1790735afb03a4b69edc"
+  integrity sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==
   dependencies:
-    "@babel/parser" "^7.24.4"
-    "@vue/shared" "3.4.27"
+    "@babel/parser" "^7.25.3"
+    "@vue/shared" "3.5.11"
     entities "^4.5.0"
     estree-walker "^2.0.2"
     source-map-js "^1.2.0"
 
-"@vue/compiler-dom@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz#d51d35f40d00ce235d7afc6ad8b09dfd92b1cc1c"
-  integrity sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==
+"@vue/compiler-dom@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz#950f8fc610e26326fed008b8d102cc8ee78a6ce5"
+  integrity sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==
   dependencies:
-    "@vue/compiler-core" "3.4.27"
-    "@vue/shared" "3.4.27"
+    "@vue/compiler-core" "3.5.11"
+    "@vue/shared" "3.5.11"
 
-"@vue/compiler-sfc@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz#399cac1b75c6737bf5440dc9cf3c385bb2959701"
-  integrity sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==
+"@vue/compiler-sfc@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz#68ba7bc6fed4fec6892aed118cb3ee8e4b180d06"
+  integrity sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==
   dependencies:
-    "@babel/parser" "^7.24.4"
-    "@vue/compiler-core" "3.4.27"
-    "@vue/compiler-dom" "3.4.27"
-    "@vue/compiler-ssr" "3.4.27"
-    "@vue/shared" "3.4.27"
+    "@babel/parser" "^7.25.3"
+    "@vue/compiler-core" "3.5.11"
+    "@vue/compiler-dom" "3.5.11"
+    "@vue/compiler-ssr" "3.5.11"
+    "@vue/shared" "3.5.11"
     estree-walker "^2.0.2"
-    magic-string "^0.30.10"
-    postcss "^8.4.38"
+    magic-string "^0.30.11"
+    postcss "^8.4.47"
     source-map-js "^1.2.0"
 
-"@vue/compiler-ssr@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz#2a8ecfef1cf448b09be633901a9c020360472e3d"
-  integrity sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==
+"@vue/compiler-ssr@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz#02d9891c7a649bbf06490ecd8d24dd1575d53e60"
+  integrity sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==
   dependencies:
-    "@vue/compiler-dom" "3.4.27"
-    "@vue/shared" "3.4.27"
+    "@vue/compiler-dom" "3.5.11"
+    "@vue/shared" "3.5.11"
 
-"@vue/devtools-api@^7.0.27":
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.1.3.tgz#b1cc9050025022193204ad804a9669e384fee8b0"
-  integrity sha512-W8IwFJ/o5iUk78jpqhvScbgCsPiOp2uileDVC0NDtW38gCWhsnu9SeBTjcdu3lbwLdsjc+H1c5Msd/x9ApbcFA==
+"@vue/devtools-api@^7.4.6":
+  version "7.4.6"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.4.6.tgz#5e9249d6de3cee58624e511fdc727837b1f2d273"
+  integrity sha512-XipBV5k0/IfTr0sNBDTg7OBUCp51cYMMXyPxLXJZ4K/wmUeMqt8cVdr2ZZGOFq+si/jTyCYnNxeKoyev5DOUUA==
   dependencies:
-    "@vue/devtools-kit" "^7.1.3"
+    "@vue/devtools-kit" "^7.4.6"
 
-"@vue/devtools-kit@^7.1.3":
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.1.3.tgz#0344fd1a926ff535d3be3378e1da8bb71d8430b9"
-  integrity sha512-NFskFSJMVCBXTkByuk2llzI3KD3Blcm7WqiRorWjD6nClHPgkH5BobDH08rfulqq5ocRt5xV+3qOT1Q9FXJrwQ==
+"@vue/devtools-kit@^7.4.6":
+  version "7.4.6"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.4.6.tgz#80aa30db65bf5b2b0eda4e818749d3c37d80f709"
+  integrity sha512-NbYBwPWgEic1AOd9bWExz9weBzFdjiIfov0yRn4DrRfR+EQJCI9dn4I0XS7IxYGdkmUJi8mFW42LLk18WsGqew==
   dependencies:
-    "@vue/devtools-shared" "^7.1.3"
+    "@vue/devtools-shared" "^7.4.6"
+    birpc "^0.2.17"
     hookable "^5.5.3"
     mitt "^3.0.1"
     perfect-debounce "^1.0.0"
     speakingurl "^14.0.1"
+    superjson "^2.2.1"
 
-"@vue/devtools-shared@^7.1.3":
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.1.3.tgz#f570bba72d53a0c84d1faa19f4d1a29a339c1dc7"
-  integrity sha512-KJ3AfgjTn3tJz/XKF+BlVShNPecim3G21oHRue+YQOsooW+0s+qXvm09U09aO7yBza5SivL1QgxSrzAbiKWjhQ==
+"@vue/devtools-shared@^7.4.6":
+  version "7.4.6"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.4.6.tgz#492c2301caacc83a32542dd95dfcae3980621417"
+  integrity sha512-rPeSBzElnHYMB05Cc056BQiJpgocQjY8XVulgni+O9a9Gr9tNXgPteSzFFD+fT/iWMxNuUgGKs9CuW5DZewfIg==
   dependencies:
-    rfdc "^1.3.1"
+    rfdc "^1.4.1"
 
-"@vue/reactivity@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.27.tgz#6ece72331bf719953f5eaa95ec60b2b8d49e3791"
-  integrity sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==
+"@vue/reactivity@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.11.tgz#d27df4fba10c2de1c7234701f18247a775b7a391"
+  integrity sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==
   dependencies:
-    "@vue/shared" "3.4.27"
+    "@vue/shared" "3.5.11"
 
-"@vue/runtime-core@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.27.tgz#1b6e1d71e4604ba7442dd25ed22e4a1fc6adbbda"
-  integrity sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==
+"@vue/runtime-core@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.11.tgz#7beccd013efe5d33981ffd6b6e05d0a5b9058316"
+  integrity sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==
   dependencies:
-    "@vue/reactivity" "3.4.27"
-    "@vue/shared" "3.4.27"
+    "@vue/reactivity" "3.5.11"
+    "@vue/shared" "3.5.11"
 
-"@vue/runtime-dom@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz#fe8d1ce9bbe8921d5dd0ad5c10df0e04ef7a5ee7"
-  integrity sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==
+"@vue/runtime-dom@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.11.tgz#14a3181ab7057de41b345b4b3d37b744b3ff8ff5"
+  integrity sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==
   dependencies:
-    "@vue/runtime-core" "3.4.27"
-    "@vue/shared" "3.4.27"
+    "@vue/reactivity" "3.5.11"
+    "@vue/runtime-core" "3.5.11"
+    "@vue/shared" "3.5.11"
     csstype "^3.1.3"
 
-"@vue/server-renderer@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.27.tgz#3306176f37e648ba665f97dda3ce705687be63d2"
-  integrity sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==
+"@vue/server-renderer@3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.11.tgz#74f558371dfc39f3b0f26f95d089a1a4d1676027"
+  integrity sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==
   dependencies:
-    "@vue/compiler-ssr" "3.4.27"
-    "@vue/shared" "3.4.27"
-
-"@vue/shared@3.4.27":
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.27.tgz#f05e3cd107d157354bb4ae7a7b5fc9cf73c63b50"
-  integrity sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==
-
-"@vueuse/core@10.9.0", "@vueuse/core@^10.9.0":
-  version "10.9.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.9.0.tgz#7d779a95cf0189de176fee63cee4ba44b3c85d64"
-  integrity sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==
+    "@vue/compiler-ssr" "3.5.11"
+    "@vue/shared" "3.5.11"
+
+"@vue/shared@3.5.11", "@vue/shared@^3.5.11":
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.11.tgz#464b840afc89be9373addff9eeb9dfc98bf3fe2e"
+  integrity sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==
+
+"@vueuse/core@11.1.0", "@vueuse/core@^11.1.0":
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-11.1.0.tgz#a104f33c899a15f3b28d3eb7b20738501a3a5035"
+  integrity sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==
   dependencies:
     "@types/web-bluetooth" "^0.0.20"
-    "@vueuse/metadata" "10.9.0"
-    "@vueuse/shared" "10.9.0"
-    vue-demi ">=0.14.7"
-
-"@vueuse/integrations@^10.9.0":
-  version "10.9.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-10.9.0.tgz#2b1a9556215ad3c1f96d39cbfbef102cf6e0ec05"
-  integrity sha512-acK+A01AYdWSvL4BZmCoJAcyHJ6EqhmkQEXbQLwev1MY7NBnS+hcEMx/BzVoR9zKI+UqEPMD9u6PsyAuiTRT4Q==
+    "@vueuse/metadata" "11.1.0"
+    "@vueuse/shared" "11.1.0"
+    vue-demi ">=0.14.10"
+
+"@vueuse/integrations@^11.1.0":
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-11.1.0.tgz#1e2c1d43b2d389fc4b4d0a7ee08091665698b9ad"
+  integrity sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA==
   dependencies:
-    "@vueuse/core" "10.9.0"
-    "@vueuse/shared" "10.9.0"
-    vue-demi ">=0.14.7"
-
-"@vueuse/metadata@10.9.0":
-  version "10.9.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.9.0.tgz#769a1a9db65daac15cf98084cbf7819ed3758620"
-  integrity sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==
-
-"@vueuse/shared@10.9.0":
-  version "10.9.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.9.0.tgz#13af2a348de15d07b7be2fd0c7fc9853a69d8fe0"
-  integrity sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==
+    "@vueuse/core" "11.1.0"
+    "@vueuse/shared" "11.1.0"
+    vue-demi ">=0.14.10"
+
+"@vueuse/metadata@11.1.0":
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-11.1.0.tgz#ad367d2a51d985129724425923b3cf95f0faf27b"
+  integrity sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==
+
+"@vueuse/shared@11.1.0":
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-11.1.0.tgz#3bfc3aa555c2a456c21945ec7f127d71938d12e8"
+  integrity sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==
   dependencies:
-    vue-demi ">=0.14.7"
+    vue-demi ">=0.14.10"
 
 algoliasearch@^4.19.1:
   version "4.23.3"
@@ -579,54 +664,98 @@ algoliasearch@^4.19.1:
     "@algolia/requester-node-http" "4.23.3"
     "@algolia/transporter" "4.23.3"
 
+birpc@^0.2.17:
+  version "0.2.19"
+  resolved "https://registry.yarnpkg.com/birpc/-/birpc-0.2.19.tgz#cdd183a4a70ba103127d49765b4a71349da5a0ca"
+  integrity sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==
+
+ccount@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
+  integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
+
+character-entities-html4@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
+  integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
+
+character-entities-legacy@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
+  integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
+
+comma-separated-tokens@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
+  integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
+
+copy-anything@^3.0.2:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
+  integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
+  dependencies:
+    is-what "^4.1.8"
+
 csstype@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
 
+dequal@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
+devlop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
+  integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+  dependencies:
+    dequal "^2.0.0"
+
 entities@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
   integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
 
-esbuild@^0.20.1:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
-  integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
+esbuild@^0.21.3:
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
+  integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
   optionalDependencies:
-    "@esbuild/aix-ppc64" "0.20.2"
-    "@esbuild/android-arm" "0.20.2"
-    "@esbuild/android-arm64" "0.20.2"
-    "@esbuild/android-x64" "0.20.2"
-    "@esbuild/darwin-arm64" "0.20.2"
-    "@esbuild/darwin-x64" "0.20.2"
-    "@esbuild/freebsd-arm64" "0.20.2"
-    "@esbuild/freebsd-x64" "0.20.2"
-    "@esbuild/linux-arm" "0.20.2"
-    "@esbuild/linux-arm64" "0.20.2"
-    "@esbuild/linux-ia32" "0.20.2"
-    "@esbuild/linux-loong64" "0.20.2"
-    "@esbuild/linux-mips64el" "0.20.2"
-    "@esbuild/linux-ppc64" "0.20.2"
-    "@esbuild/linux-riscv64" "0.20.2"
-    "@esbuild/linux-s390x" "0.20.2"
-    "@esbuild/linux-x64" "0.20.2"
-    "@esbuild/netbsd-x64" "0.20.2"
-    "@esbuild/openbsd-x64" "0.20.2"
-    "@esbuild/sunos-x64" "0.20.2"
-    "@esbuild/win32-arm64" "0.20.2"
-    "@esbuild/win32-ia32" "0.20.2"
-    "@esbuild/win32-x64" "0.20.2"
+    "@esbuild/aix-ppc64" "0.21.5"
+    "@esbuild/android-arm" "0.21.5"
+    "@esbuild/android-arm64" "0.21.5"
+    "@esbuild/android-x64" "0.21.5"
+    "@esbuild/darwin-arm64" "0.21.5"
+    "@esbuild/darwin-x64" "0.21.5"
+    "@esbuild/freebsd-arm64" "0.21.5"
+    "@esbuild/freebsd-x64" "0.21.5"
+    "@esbuild/linux-arm" "0.21.5"
+    "@esbuild/linux-arm64" "0.21.5"
+    "@esbuild/linux-ia32" "0.21.5"
+    "@esbuild/linux-loong64" "0.21.5"
+    "@esbuild/linux-mips64el" "0.21.5"
+    "@esbuild/linux-ppc64" "0.21.5"
+    "@esbuild/linux-riscv64" "0.21.5"
+    "@esbuild/linux-s390x" "0.21.5"
+    "@esbuild/linux-x64" "0.21.5"
+    "@esbuild/netbsd-x64" "0.21.5"
+    "@esbuild/openbsd-x64" "0.21.5"
+    "@esbuild/sunos-x64" "0.21.5"
+    "@esbuild/win32-arm64" "0.21.5"
+    "@esbuild/win32-ia32" "0.21.5"
+    "@esbuild/win32-x64" "0.21.5"
 
 estree-walker@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
-focus-trap@^7.5.4:
-  version "7.5.4"
-  resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.5.4.tgz#6c4e342fe1dae6add9c2aa332a6e7a0bbd495ba2"
-  integrity sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==
+focus-trap@^7.6.0:
+  version "7.6.0"
+  resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.6.0.tgz#7f3edab8135eaca92ab59b6e963eb5cc42ded715"
+  integrity sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==
   dependencies:
     tabbable "^6.2.0"
 
@@ -635,27 +764,108 @@ fsevents@~2.3.2, fsevents@~2.3.3:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
   integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
 
+hast-util-to-html@^9.0.3:
+  version "9.0.3"
+  resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b"
+  integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    "@types/unist" "^3.0.0"
+    ccount "^2.0.0"
+    comma-separated-tokens "^2.0.0"
+    hast-util-whitespace "^3.0.0"
+    html-void-elements "^3.0.0"
+    mdast-util-to-hast "^13.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+    stringify-entities "^4.0.0"
+    zwitch "^2.0.4"
+
+hast-util-whitespace@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
+  integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
+  dependencies:
+    "@types/hast" "^3.0.0"
+
 hookable@^5.5.3:
   version "5.5.3"
   resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
   integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
 
-magic-string@^0.30.10:
-  version "0.30.10"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
-  integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
+html-void-elements@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
+  integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
+
+is-what@^4.1.8:
+  version "4.1.16"
+  resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
+  integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
+
+magic-string@^0.30.11:
+  version "0.30.11"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954"
+  integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==
   dependencies:
-    "@jridgewell/sourcemap-codec" "^1.4.15"
+    "@jridgewell/sourcemap-codec" "^1.5.0"
 
 mark.js@8.11.1:
   version "8.11.1"
   resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
   integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==
 
-minisearch@^6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-6.3.0.tgz#985a2f1ca3c73c2d65af94f0616bfe57164b0b6b"
-  integrity sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==
+mdast-util-to-hast@^13.0.0:
+  version "13.2.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
+  integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
+  dependencies:
+    "@types/hast" "^3.0.0"
+    "@types/mdast" "^4.0.0"
+    "@ungap/structured-clone" "^1.0.0"
+    devlop "^1.0.0"
+    micromark-util-sanitize-uri "^2.0.0"
+    trim-lines "^3.0.0"
+    unist-util-position "^5.0.0"
+    unist-util-visit "^5.0.0"
+    vfile "^6.0.0"
+
+micromark-util-character@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1"
+  integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
+micromark-util-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
+  integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
+
+micromark-util-sanitize-uri@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
+  integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+
+micromark-util-symbol@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
+  integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
+
+micromark-util-types@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
+  integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
+
+minisearch@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.1.0.tgz#f5830e9109b5919ee7b291c29a304f381aa68770"
+  integrity sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==
 
 mitt@^3.0.1:
   version "3.0.1"
@@ -663,130 +873,248 @@ mitt@^3.0.1:
   integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
 
 nanoid@^3.3.7:
-  version "3.3.7"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
-  integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+  version "3.3.8"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
+  integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+
+oniguruma-to-js@0.4.3:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740"
+  integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==
+  dependencies:
+    regex "^4.3.2"
 
 perfect-debounce@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
   integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
 
-picocolors@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
-  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+picocolors@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
+  integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
 
-postcss@^8.4.38:
-  version "8.4.38"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
-  integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+postcss@^8.4.43, postcss@^8.4.47:
+  version "8.4.47"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
+  integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
   dependencies:
     nanoid "^3.3.7"
-    picocolors "^1.0.0"
-    source-map-js "^1.2.0"
+    picocolors "^1.1.0"
+    source-map-js "^1.2.1"
 
 preact@^10.0.0:
   version "10.21.0"
   resolved "https://registry.yarnpkg.com/preact/-/preact-10.21.0.tgz#5b0335c873a1724deb66e517830db4fd310c24f6"
   integrity sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg==
 
-rfdc@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
-  integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==
-
-rollup@^4.13.0:
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.17.2.tgz#26d1785d0144122277fdb20ab3a24729ae68301f"
-  integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==
+property-information@^6.0.0:
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
+  integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
+
+regex@^4.3.2:
+  version "4.3.3"
+  resolved "https://registry.yarnpkg.com/regex/-/regex-4.3.3.tgz#8cda73ccbdfa7c5691881d02f9bb142dba9daa6a"
+  integrity sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==
+
+rfdc@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
+  integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
+
+rollup@^4.20.0:
+  version "4.24.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05"
+  integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
   dependencies:
-    "@types/estree" "1.0.5"
+    "@types/estree" "1.0.6"
   optionalDependencies:
-    "@rollup/rollup-android-arm-eabi" "4.17.2"
-    "@rollup/rollup-android-arm64" "4.17.2"
-    "@rollup/rollup-darwin-arm64" "4.17.2"
-    "@rollup/rollup-darwin-x64" "4.17.2"
-    "@rollup/rollup-linux-arm-gnueabihf" "4.17.2"
-    "@rollup/rollup-linux-arm-musleabihf" "4.17.2"
-    "@rollup/rollup-linux-arm64-gnu" "4.17.2"
-    "@rollup/rollup-linux-arm64-musl" "4.17.2"
-    "@rollup/rollup-linux-powerpc64le-gnu" "4.17.2"
-    "@rollup/rollup-linux-riscv64-gnu" "4.17.2"
-    "@rollup/rollup-linux-s390x-gnu" "4.17.2"
-    "@rollup/rollup-linux-x64-gnu" "4.17.2"
-    "@rollup/rollup-linux-x64-musl" "4.17.2"
-    "@rollup/rollup-win32-arm64-msvc" "4.17.2"
-    "@rollup/rollup-win32-ia32-msvc" "4.17.2"
-    "@rollup/rollup-win32-x64-msvc" "4.17.2"
+    "@rollup/rollup-android-arm-eabi" "4.24.0"
+    "@rollup/rollup-android-arm64" "4.24.0"
+    "@rollup/rollup-darwin-arm64" "4.24.0"
+    "@rollup/rollup-darwin-x64" "4.24.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.24.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.24.0"
+    "@rollup/rollup-linux-arm64-musl" "4.24.0"
+    "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.24.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.24.0"
+    "@rollup/rollup-linux-x64-gnu" "4.24.0"
+    "@rollup/rollup-linux-x64-musl" "4.24.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.24.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.24.0"
+    "@rollup/rollup-win32-x64-msvc" "4.24.0"
     fsevents "~2.3.2"
 
-shiki@1.5.0, shiki@^1.3.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.5.0.tgz#32c5f2f6233a84e8f798ee7a2762dbbee3a6d7c3"
-  integrity sha512-AMax9zrUW8u8bnvNhnmAD9mHzk244mWCDBZm+zh4Ir3lzncF/sGUcVd5gpy0IlWvOKBUUJ8uu/BFpusGJ/PdVw==
+shiki@1.22.0, shiki@^1.22.0:
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.0.tgz#45d1dfff0e03a598af70e2ec8592f14ef07827b4"
+  integrity sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==
   dependencies:
-    "@shikijs/core" "1.5.0"
+    "@shikijs/core" "1.22.0"
+    "@shikijs/engine-javascript" "1.22.0"
+    "@shikijs/engine-oniguruma" "1.22.0"
+    "@shikijs/types" "1.22.0"
+    "@shikijs/vscode-textmate" "^9.3.0"
+    "@types/hast" "^3.0.4"
 
 source-map-js@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
   integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
 
+source-map-js@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+  integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+space-separated-tokens@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
+  integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
+
 speakingurl@^14.0.1:
   version "14.0.1"
   resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
   integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
 
+stringify-entities@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3"
+  integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
+  dependencies:
+    character-entities-html4 "^2.0.0"
+    character-entities-legacy "^3.0.0"
+
+superjson@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.1.tgz#9377a7fa80fedb10c851c9dbffd942d4bcf79733"
+  integrity sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==
+  dependencies:
+    copy-anything "^3.0.2"
+
 tabbable@^6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
   integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
 
-vite@^5.2.10:
-  version "5.2.11"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd"
-  integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+
+trim-lines@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
+  integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
+
+unist-util-is@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
+  integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
+unist-util-position@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
+  integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
+unist-util-stringify-position@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
+  integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
+unist-util-visit-parents@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
+  integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+
+unist-util-visit@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
+  integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+    unist-util-visit-parents "^6.0.0"
+
+vfile-message@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
+  integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-stringify-position "^4.0.0"
+
+vfile@^6.0.0:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
+  integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
   dependencies:
-    esbuild "^0.20.1"
-    postcss "^8.4.38"
-    rollup "^4.13.0"
+    "@types/unist" "^3.0.0"
+    vfile-message "^4.0.0"
+
+vite@^5.4.8:
+  version "5.4.14"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
+  integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
+  dependencies:
+    esbuild "^0.21.3"
+    postcss "^8.4.43"
+    rollup "^4.20.0"
   optionalDependencies:
     fsevents "~2.3.3"
 
-vitepress@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.1.4.tgz#f715acab50059e0c75d1414fa1b8f31762f43804"
-  integrity sha512-bWIzFZXpPB6NIDBuWnS20aMADH+FcFKDfQNYFvbOWij03PR29eImTceQHIzCKordjXYBhM/TjE5VKFTUJ3EheA==
-  dependencies:
-    "@docsearch/css" "^3.6.0"
-    "@docsearch/js" "^3.6.0"
-    "@shikijs/core" "^1.3.0"
-    "@shikijs/transformers" "^1.3.0"
-    "@types/markdown-it" "^14.0.1"
-    "@vitejs/plugin-vue" "^5.0.4"
-    "@vue/devtools-api" "^7.0.27"
-    "@vueuse/core" "^10.9.0"
-    "@vueuse/integrations" "^10.9.0"
-    focus-trap "^7.5.4"
+vitepress@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.4.0.tgz#5e879230d98e5c4e5aec91daade6945bbc18934b"
+  integrity sha512-JXCv4EsKTDyAFb6C/UjZr7nsGAzZ6mafVk2rx7rG5o8N+B/4QstIk+iEOe/9dKoU6V624UIC6g1pZ+K63rxhlw==
+  dependencies:
+    "@docsearch/css" "^3.6.2"
+    "@docsearch/js" "^3.6.2"
+    "@shikijs/core" "^1.22.0"
+    "@shikijs/transformers" "^1.22.0"
+    "@shikijs/types" "^1.22.0"
+    "@types/markdown-it" "^14.1.2"
+    "@vitejs/plugin-vue" "^5.1.4"
+    "@vue/devtools-api" "^7.4.6"
+    "@vue/shared" "^3.5.11"
+    "@vueuse/core" "^11.1.0"
+    "@vueuse/integrations" "^11.1.0"
+    focus-trap "^7.6.0"
     mark.js "8.11.1"
-    minisearch "^6.3.0"
-    shiki "^1.3.0"
-    vite "^5.2.10"
-    vue "^3.4.25"
-
-vue-demi@>=0.14.7:
-  version "0.14.7"
-  resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
-  integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
-
-vue@^3.4.25:
-  version "3.4.27"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.27.tgz#40b7d929d3e53f427f7f5945386234d2854cc2a1"
-  integrity sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==
-  dependencies:
-    "@vue/compiler-dom" "3.4.27"
-    "@vue/compiler-sfc" "3.4.27"
-    "@vue/runtime-dom" "3.4.27"
-    "@vue/server-renderer" "3.4.27"
-    "@vue/shared" "3.4.27"
+    minisearch "^7.1.0"
+    shiki "^1.22.0"
+    vite "^5.4.8"
+    vue "^3.5.11"
+
+vue-demi@>=0.14.10:
+  version "0.14.10"
+  resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
+  integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
+
+vue@^3.5.11:
+  version "3.5.11"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.11.tgz#3e307183797629f701e303a0a008f517ae031483"
+  integrity sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==
+  dependencies:
+    "@vue/compiler-dom" "3.5.11"
+    "@vue/compiler-sfc" "3.5.11"
+    "@vue/runtime-dom" "3.5.11"
+    "@vue/server-renderer" "3.5.11"
+    "@vue/shared" "3.5.11"
+
+zwitch@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
+  integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js
index ccb2978a8..ebddd7807 100644
--- a/frontend/js/app/controller.js
+++ b/frontend/js/app/controller.js
@@ -4,444 +4,438 @@ const Tokens   = require('./tokens');
 
 module.exports = {
 
-    /**
-     * @param {String} route
-     * @param {Object} [options]
-     * @returns {Boolean}
-     */
-    navigate: function (route, options) {
-        options = options || {};
-        Backbone.history.navigate(route.toString(), options);
-        return true;
-    },
-
-    /**
-     * Login
-     */
-    showLogin: function () {
-        window.location = '/login';
-    },
-
-    /**
-     * Users
-     */
-    showUsers: function () {
-        let controller = this;
-        if (Cache.User.isAdmin()) {
-            require(['./main', './users/main'], (App, View) => {
-                controller.navigate('/users');
-                App.UI.showAppContent(new View());
-            });
-        } else {
-            this.showDashboard();
-        }
-    },
-
-    /**
-     * User Form
-     *
-     * @param [model]
-     */
-    showUserForm: function (model) {
-        if (Cache.User.isAdmin()) {
-            require(['./main', './user/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * User Permissions Form
-     *
-     * @param model
-     */
-    showUserPermissions: function (model) {
-        if (Cache.User.isAdmin()) {
-            require(['./main', './user/permissions'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * User Password Form
-     *
-     * @param model
-     */
-    showUserPasswordForm: function (model) {
-        if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
-            require(['./main', './user/password'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * User Delete Confirm
-     *
-     * @param model
-     */
-    showUserDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
-            require(['./main', './user/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Dashboard
-     */
-    showDashboard: function () {
-        let controller = this;
-
-        require(['./main', './dashboard/main'], (App, View) => {
-            controller.navigate('/');
-            App.UI.showAppContent(new View());
-        });
-    },
-
-    /**
-     * Nginx Proxy Hosts
-     */
-    showNginxProxy: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
-            let controller = this;
-
-            require(['./main', './nginx/proxy/main'], (App, View) => {
-                controller.navigate('/nginx/proxy');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Nginx Proxy Host Form
-     *
-     * @param [model]
-     */
-    showNginxProxyForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
-            require(['./main', './nginx/proxy/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Proxy Host Delete Confirm
-     *
-     * @param model
-     */
-    showNginxProxyDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
-            require(['./main', './nginx/proxy/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Nginx Redirection Hosts
-     */
-    showNginxRedirection: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
-            let controller = this;
-
-            require(['./main', './nginx/redirection/main'], (App, View) => {
-                controller.navigate('/nginx/redirection');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Nginx Redirection Host Form
-     *
-     * @param [model]
-     */
-    showNginxRedirectionForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
-            require(['./main', './nginx/redirection/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Proxy Redirection Delete Confirm
-     *
-     * @param model
-     */
-    showNginxRedirectionDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
-            require(['./main', './nginx/redirection/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Nginx Stream Hosts
-     */
-    showNginxStream: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
-            let controller = this;
-
-            require(['./main', './nginx/stream/main'], (App, View) => {
-                controller.navigate('/nginx/stream');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Stream Form
-     *
-     * @param [model]
-     */
-    showNginxStreamForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
-            require(['./main', './nginx/stream/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Stream Delete Confirm
-     *
-     * @param model
-     */
-    showNginxStreamDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
-            require(['./main', './nginx/stream/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Nginx Dead Hosts
-     */
-    showNginxDead: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
-            let controller = this;
-
-            require(['./main', './nginx/dead/main'], (App, View) => {
-                controller.navigate('/nginx/404');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Dead Host Form
-     *
-     * @param [model]
-     */
-    showNginxDeadForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
-            require(['./main', './nginx/dead/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Dead Host Delete Confirm
-     *
-     * @param model
-     */
-    showNginxDeadDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
-            require(['./main', './nginx/dead/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Help Dialog
-     *
-     * @param {String}  title
-     * @param {String}  content
-     */
-    showHelp: function (title, content) {
-        require(['./main', './help/main'], function (App, View) {
-            App.UI.showModalDialog(new View({title: title, content: content}));
-        });
-    },
-
-    /**
-     * Nginx Access
-     */
-    showNginxAccess: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
-            let controller = this;
-
-            require(['./main', './nginx/access/main'], (App, View) => {
-                controller.navigate('/nginx/access');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Nginx Access List Form
-     *
-     * @param [model]
-     */
-    showNginxAccessListForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
-            require(['./main', './nginx/access/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Access List Delete Confirm
-     *
-     * @param model
-     */
-    showNginxAccessListDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
-            require(['./main', './nginx/access/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Nginx Certificates
-     */
-    showNginxCertificates: function () {
-        if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
-            let controller = this;
-
-            require(['./main', './nginx/certificates/main'], (App, View) => {
-                controller.navigate('/nginx/certificates');
-                App.UI.showAppContent(new View());
-            });
-        }
-    },
-
-    /**
-     * Nginx Certificate Form
-     *
-     * @param [model]
-     */
-    showNginxCertificateForm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
-            require(['./main', './nginx/certificates/form'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Certificate Renew
-     *
-     * @param model
-     */
-    showNginxCertificateRenew: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
-            require(['./main', './nginx/certificates/renew'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Certificate Delete Confirm
-     *
-     * @param model
-     */
-    showNginxCertificateDeleteConfirm: function (model) {
-        if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
-            require(['./main', './nginx/certificates/delete'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Certificate Test Reachability
-     *
-     * @param model
-     */
-    showNginxCertificateTestReachability: function (model) {
-      if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
-        require(['./main', './nginx/certificates/test'], function (App, View) {
-          App.UI.showModalDialog(new View({model: model}));
-        });
-      }
-    },
-
-    /**
-     * Audit Log
-     */
-    showAuditLog: function () {
-        let controller = this;
-        if (Cache.User.isAdmin()) {
-            require(['./main', './audit-log/main'], (App, View) => {
-                controller.navigate('/audit-log');
-                App.UI.showAppContent(new View());
-            });
-        } else {
-            this.showDashboard();
-        }
-    },
-
-    /**
-     * Audit Log Metadata
-     *
-     * @param model
-     */
-    showAuditMeta: function (model) {
-        if (Cache.User.isAdmin()) {
-            require(['./main', './audit-log/meta'], function (App, View) {
-                App.UI.showModalDialog(new View({model: model}));
-            });
-        }
-    },
-
-    /**
-     * Settings
-     */
-    showSettings: function () {
-        let controller = this;
-        if (Cache.User.isAdmin()) {
-            require(['./main', './settings/main'], (App, View) => {
-                controller.navigate('/settings');
-                App.UI.showAppContent(new View());
-            });
-        } else {
-            this.showDashboard();
-        }
-    },
-
-    /**
-     * Settings Item Form
-     *
-     * @param model
-     */
-    showSettingForm: function (model) {
-        if (Cache.User.isAdmin()) {
-            if (model.get('id') === 'default-site') {
-                require(['./main', './settings/default-site/main'], function (App, View) {
-                    App.UI.showModalDialog(new View({model: model}));
-                });
-            }
-        }
-    },
-
-    /**
-     * Logout
-     */
-    logout: function () {
-        Tokens.dropTopToken();
-        this.showLogin();
-    }
+	/**
+	 * @param {String} route
+	 * @param {Object} [options]
+	 * @returns {Boolean}
+	 */
+	navigate: function (route, options) {
+		options = options || {};
+		Backbone.history.navigate(route.toString(), options);
+		return true;
+	},
+
+	/**
+	 * Login
+	 */
+	showLogin: function () {
+		window.location = '/login';
+	},
+
+	/**
+	 * Users
+	 */
+	showUsers: function () {
+		const controller = this;
+		if (Cache.User.isAdmin()) {
+			require(['./main', './users/main'], (App, View) => {
+				controller.navigate('/users');
+				App.UI.showAppContent(new View());
+			});
+		} else {
+			this.showDashboard();
+		}
+	},
+
+	/**
+	 * User Form
+	 *
+	 * @param [model]
+	 */
+	showUserForm: function (model) {
+		if (Cache.User.isAdmin()) {
+			require(['./main', './user/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * User Permissions Form
+	 *
+	 * @param model
+	 */
+	showUserPermissions: function (model) {
+		if (Cache.User.isAdmin()) {
+			require(['./main', './user/permissions'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * User Password Form
+	 *
+	 * @param model
+	 */
+	showUserPasswordForm: function (model) {
+		if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
+			require(['./main', './user/password'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * User Delete Confirm
+	 *
+	 * @param model
+	 */
+	showUserDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
+			require(['./main', './user/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Dashboard
+	 */
+	showDashboard: function () {
+		const controller = this;
+		require(['./main', './dashboard/main'], (App, View) => {
+			controller.navigate('/');
+			App.UI.showAppContent(new View());
+		});
+	},
+
+	/**
+	 * Nginx Proxy Hosts
+	 */
+	showNginxProxy: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
+			const controller = this;
+
+			require(['./main', './nginx/proxy/main'], (App, View) => {
+				controller.navigate('/nginx/proxy');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Nginx Proxy Host Form
+	 *
+	 * @param [model]
+	 */
+	showNginxProxyForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
+			require(['./main', './nginx/proxy/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Proxy Host Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxProxyDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
+			require(['./main', './nginx/proxy/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Nginx Redirection Hosts
+	 */
+	showNginxRedirection: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
+			const controller = this;
+			require(['./main', './nginx/redirection/main'], (App, View) => {
+				controller.navigate('/nginx/redirection');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Nginx Redirection Host Form
+	 *
+	 * @param [model]
+	 */
+	showNginxRedirectionForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
+			require(['./main', './nginx/redirection/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Proxy Redirection Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxRedirectionDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
+			require(['./main', './nginx/redirection/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Nginx Stream Hosts
+	 */
+	showNginxStream: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
+			const controller = this;
+			require(['./main', './nginx/stream/main'], (App, View) => {
+				controller.navigate('/nginx/stream');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Stream Form
+	 *
+	 * @param [model]
+	 */
+	showNginxStreamForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
+			require(['./main', './nginx/stream/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Stream Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxStreamDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
+			require(['./main', './nginx/stream/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Nginx Dead Hosts
+	 */
+	showNginxDead: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
+			const controller = this;
+			require(['./main', './nginx/dead/main'], (App, View) => {
+				controller.navigate('/nginx/404');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Dead Host Form
+	 *
+	 * @param [model]
+	 */
+	showNginxDeadForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
+			require(['./main', './nginx/dead/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Dead Host Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxDeadDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
+			require(['./main', './nginx/dead/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Help Dialog
+	 *
+	 * @param {String}  title
+	 * @param {String}  content
+	 */
+	showHelp: function (title, content) {
+		require(['./main', './help/main'], function (App, View) {
+			App.UI.showModalDialog(new View({title: title, content: content}));
+		});
+	},
+
+	/**
+	 * Nginx Access
+	 */
+	showNginxAccess: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
+			const controller = this;
+			require(['./main', './nginx/access/main'], (App, View) => {
+				controller.navigate('/nginx/access');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Nginx Access List Form
+	 *
+	 * @param [model]
+	 */
+	showNginxAccessListForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
+			require(['./main', './nginx/access/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Access List Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxAccessListDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
+			require(['./main', './nginx/access/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Nginx Certificates
+	 */
+	showNginxCertificates: function () {
+		if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
+			const controller = this;
+			require(['./main', './nginx/certificates/main'], (App, View) => {
+				controller.navigate('/nginx/certificates');
+				App.UI.showAppContent(new View());
+			});
+		}
+	},
+
+	/**
+	 * Nginx Certificate Form
+	 *
+	 * @param [model]
+	 */
+	showNginxCertificateForm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
+			require(['./main', './nginx/certificates/form'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Certificate Renew
+	 *
+	 * @param model
+	 */
+	showNginxCertificateRenew: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
+			require(['./main', './nginx/certificates/renew'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Certificate Delete Confirm
+	 *
+	 * @param model
+	 */
+	showNginxCertificateDeleteConfirm: function (model) {
+		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
+			require(['./main', './nginx/certificates/delete'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Certificate Test Reachability
+	 *
+	 * @param model
+	 */
+	showNginxCertificateTestReachability: function (model) {
+	  if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
+		require(['./main', './nginx/certificates/test'], function (App, View) {
+		  App.UI.showModalDialog(new View({model: model}));
+		});
+	  }
+	},
+
+	/**
+	 * Audit Log
+	 */
+	showAuditLog: function () {
+		const controller = this;
+		if (Cache.User.isAdmin()) {
+			require(['./main', './audit-log/main'], (App, View) => {
+				controller.navigate('/audit-log');
+				App.UI.showAppContent(new View());
+			});
+		} else {
+			this.showDashboard();
+		}
+	},
+
+	/**
+	 * Audit Log Metadata
+	 *
+	 * @param model
+	 */
+	showAuditMeta: function (model) {
+		if (Cache.User.isAdmin()) {
+			require(['./main', './audit-log/meta'], function (App, View) {
+				App.UI.showModalDialog(new View({model: model}));
+			});
+		}
+	},
+
+	/**
+	 * Settings
+	 */
+	showSettings: function () {
+		const controller = this;
+		if (Cache.User.isAdmin()) {
+			require(['./main', './settings/main'], (App, View) => {
+				controller.navigate('/settings');
+				App.UI.showAppContent(new View());
+			});
+		} else {
+			this.showDashboard();
+		}
+	},
+
+	/**
+	 * Settings Item Form
+	 *
+	 * @param model
+	 */
+	showSettingForm: function (model) {
+		if (Cache.User.isAdmin()) {
+			if (model.get('id') === 'default-site') {
+				require(['./main', './settings/default-site/main'], function (App, View) {
+					App.UI.showModalDialog(new View({model: model}));
+				});
+			}
+		}
+	},
+
+	/**
+	 * Logout
+	 */
+	logout: function () {
+		Tokens.dropTopToken();
+		this.showLogin();
+	}
 };
diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js
index c2e82f855..ba4a99a67 100644
--- a/frontend/js/app/dashboard/main.js
+++ b/frontend/js/app/dashboard/main.js
@@ -6,87 +6,85 @@ const Helpers    = require('../../lib/helpers');
 const template   = require('./main.ejs');
 
 module.exports = Mn.View.extend({
-    template: template,
-    id:       'dashboard',
-    columns:  0,
-
-    stats: {},
-
-    ui: {
-        links: 'a'
-    },
-
-    events: {
-        'click @ui.links': function (e) {
-            e.preventDefault();
-            Controller.navigate($(e.currentTarget).attr('href'), true);
-        }
-    },
-
-    templateContext: function () {
-        let view = this;
-
-        return {
-            getUserName: function () {
-                return Cache.User.get('nickname') || Cache.User.get('name');
-            },
-
-            getHostStat: function (type) {
-                if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
-                    return Helpers.niceNumber(view.stats.hosts[type]);
-                }
-
-                return '-';
-            },
-
-            canShow: function (perm) {
-                return Cache.User.isAdmin() || Cache.User.canView(perm);
-            },
-
-            columns: view.columns
-        };
-    },
-
-    onRender: function () {
-        let view = this;
-
-        if (typeof view.stats.hosts === 'undefined') {
-            Api.Reports.getHostStats()
-                .then(response => {
-                    if (!view.isDestroyed()) {
-                        view.stats.hosts = response;
-                        view.render();
-                    }
-                })
-                .catch(err => {
-                    console.log(err);
-                });
-        }
-    },
-
-    /**
-     * @param {Object}  [model]
-     */
-    preRender: function (model) {
-        this.columns = 0;
-
-        // calculate the available columns based on permissions for the objects
-        // and store as a variable
-        //let view = this;
-        let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
-
-        perms.map(perm => {
-            this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
-        });
-
-        // Prevent double rendering on initial calls
-        if (typeof model !== 'undefined') {
-            this.render();
-        }
-    },
-
-    initialize: function () {
-        this.preRender();
-        this.listenTo(Cache.User, 'change', this.preRender);
-    }
+	template: template,
+	id:       'dashboard',
+	columns:  0,
+
+	stats: {},
+
+	ui: {
+		links: 'a'
+	},
+
+	events: {
+		'click @ui.links': function (e) {
+			e.preventDefault();
+			Controller.navigate($(e.currentTarget).attr('href'), true);
+		}
+	},
+
+	templateContext: function () {
+		const view = this;
+
+		return {
+			getUserName: function () {
+				return Cache.User.get('nickname') || Cache.User.get('name');
+			},
+
+			getHostStat: function (type) {
+				if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
+					return Helpers.niceNumber(view.stats.hosts[type]);
+				}
+
+				return '-';
+			},
+
+			canShow: function (perm) {
+				return Cache.User.isAdmin() || Cache.User.canView(perm);
+			},
+
+			columns: view.columns
+		};
+	},
+
+	onRender: function () {
+		const view = this;
+		if (typeof view.stats.hosts === 'undefined') {
+			Api.Reports.getHostStats()
+				.then(response => {
+					if (!view.isDestroyed()) {
+						view.stats.hosts = response;
+						view.render();
+					}
+				})
+				.catch(err => {
+					console.log(err);
+				});
+		}
+	},
+
+	/**
+	 * @param {Object}  [model]
+	 */
+	preRender: function (model) {
+		this.columns = 0;
+
+		// calculate the available columns based on permissions for the objects
+		// and store as a variable
+		const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
+
+		perms.map(perm => {
+			this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
+		});
+
+		// Prevent double rendering on initial calls
+		if (typeof model !== 'undefined') {
+			this.render();
+		}
+	},
+
+	initialize: function () {
+		this.preRender();
+		this.listenTo(Cache.User, 'change', this.preRender);
+	}
 });
diff --git a/frontend/js/app/nginx/access/list/item.ejs b/frontend/js/app/nginx/access/list/item.ejs
index 2ee37a50a..fe043c98e 100644
--- a/frontend/js/app/nginx/access/list/item.ejs
+++ b/frontend/js/app/nginx/access/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
diff --git a/frontend/js/app/nginx/certificates/list/item.ejs b/frontend/js/app/nginx/certificates/list/item.ejs
index 20d6f2396..179a81955 100644
--- a/frontend/js/app/nginx/certificates/list/item.ejs
+++ b/frontend/js/app/nginx/certificates/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
@@ -33,6 +33,13 @@
 <td class="<%- isExpired() ? 'text-danger' : '' %>">
     <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %>
 </td>
+<td>
+    <% if (active_domain_names().length > 0) { %>
+        <span class="status-icon bg-success"></span> <%- i18n('certificates', 'in-use') %>
+    <% } else { %>
+        <span class="status-icon bg-danger"></span> <%- i18n('certificates', 'inactive') %>
+    <% } %>
+</td>
 <% if (canManage) { %>
 <td class="text-right">
     <div class="item-action dropdown">
@@ -48,7 +55,14 @@
                 <div class="dropdown-divider"></div>
             <% } %>
             <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
+            <% if (active_domain_names().length > 0) { %>
+                <div class="dropdown-divider"></div>
+                <span class="dropdown-header"><%- i18n('certificates', 'active-domain_names') %></span>
+                <% active_domain_names().forEach(function(host) { %>
+                    <a href="https://<%- host %>" class="dropdown-item" target="_blank"><%- host %></a>
+                <% }); %>
+            <% } %>
         </div>
     </div>
 </td>
-<% } %>
+<% } %>
\ No newline at end of file
diff --git a/frontend/js/app/nginx/certificates/list/item.js b/frontend/js/app/nginx/certificates/list/item.js
index 7fa1c6814..b9a927adc 100644
--- a/frontend/js/app/nginx/certificates/list/item.js
+++ b/frontend/js/app/nginx/certificates/list/item.js
@@ -44,14 +44,24 @@ module.exports = Mn.View.extend({
         },
     },
 
-    templateContext: {
-        canManage: App.Cache.User.canManage('certificates'),
-        isExpired: function () {
-            return moment(this.expires_on).isBefore(moment());
-        },
-        dns_providers: dns_providers
+    templateContext: function () {
+        return {
+            canManage: App.Cache.User.canManage('certificates'),
+            isExpired: function () {
+                return moment(this.expires_on).isBefore(moment());
+            },
+            dns_providers: dns_providers,
+            active_domain_names: function () {
+                const { proxy_hosts = [], redirect_hosts = [], dead_hosts = [] } = this;
+                return [...proxy_hosts, ...redirect_hosts, ...dead_hosts].reduce((acc, host) => {
+                    acc.push(...(host.domain_names || []));
+                    return acc;
+                }, []);
+            }
+        };
     },
 
+
     initialize: function () {
         this.listenTo(this.model, 'change', this.render);
     }
diff --git a/frontend/js/app/nginx/certificates/list/main.ejs b/frontend/js/app/nginx/certificates/list/main.ejs
index aa49a27fb..329b58434 100644
--- a/frontend/js/app/nginx/certificates/list/main.ejs
+++ b/frontend/js/app/nginx/certificates/list/main.ejs
@@ -3,6 +3,7 @@
     <th><%- i18n('str', 'name') %></th>
     <th><%- i18n('all-hosts', 'cert-provider') %></th>
     <th><%- i18n('str', 'expires') %></th>
+    <th><%- i18n('str', 'status') %></th>
     <% if (canManage) { %>
     <th>&nbsp;</th>
     <% } %>
diff --git a/frontend/js/app/nginx/certificates/main.js b/frontend/js/app/nginx/certificates/main.js
index 89562768b..3f9f022eb 100644
--- a/frontend/js/app/nginx/certificates/main.js
+++ b/frontend/js/app/nginx/certificates/main.js
@@ -74,7 +74,7 @@ module.exports = Mn.View.extend({
             e.preventDefault();
             let query = this.ui.query.val();
 
-            this.fetch(['owner'], query)
+            this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], query)
                 .then(response => this.showData(response))
                 .catch(err => {
                     this.showError(err);
@@ -89,7 +89,7 @@ module.exports = Mn.View.extend({
     onRender: function () {
         let view = this;
 
-        view.fetch(['owner'])
+        view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
             .then(response => {
                 if (!view.isDestroyed()) {
                     if (response && response.length) {
diff --git a/frontend/js/app/nginx/dead/list/item.ejs b/frontend/js/app/nginx/dead/list/item.ejs
index d447bd1e3..dede3b639 100644
--- a/frontend/js/app/nginx/dead/list/item.ejs
+++ b/frontend/js/app/nginx/dead/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
diff --git a/frontend/js/app/nginx/proxy/list/item.ejs b/frontend/js/app/nginx/proxy/list/item.ejs
index a59368048..3eeaa6d26 100644
--- a/frontend/js/app/nginx/proxy/list/item.ejs
+++ b/frontend/js/app/nginx/proxy/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
diff --git a/frontend/js/app/nginx/redirection/list/item.ejs b/frontend/js/app/nginx/redirection/list/item.ejs
index 4f25d973e..dff186fdf 100644
--- a/frontend/js/app/nginx/redirection/list/item.ejs
+++ b/frontend/js/app/nginx/redirection/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
diff --git a/frontend/js/app/nginx/stream/form.ejs b/frontend/js/app/nginx/stream/form.ejs
index 1fc4f1342..800945f36 100644
--- a/frontend/js/app/nginx/stream/form.ejs
+++ b/frontend/js/app/nginx/stream/form.ejs
@@ -3,48 +3,187 @@
         <h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
     </div>
-    <div class="modal-body">
+    <div class="modal-body has-tabs">
+        <div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
         <form>
-            <div class="row">
-                <div class="col-sm-12 col-md-12">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
-                        <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
+                <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
+            </ul>
+            <div class="tab-content">
+                <!-- Details -->
+                <div role="tabpanel" class="tab-pane active" id="details">
+                    <div class="row">
+                        <div class="col-sm-12 col-md-12">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
+                                <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-8 col-md-8">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
+                                <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-4 col-md-4">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
+                                <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12">
+                            <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
+                        </div>
                     </div>
                 </div>
-                <div class="col-sm-8 col-md-8">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
-                        <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
+
+                <!-- SSL -->
+                <div role="tabpanel" class="tab-pane" id="ssl-options">
+                    <div class="row">
+                        <div class="col-sm-12 col-md-12">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label>
+                                <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
+                                    <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
+                                    <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
+                                </select>
+                            </div>
+                        </div>
+
+                        <!-- DNS challenge -->
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label>
+                                <input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>">
+                            </div>
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input
+                                            type="checkbox"
+                                            class="custom-switch-input"
+                                            name="meta[dns_challenge]"
+                                            value="1"
+                                            checked
+                                            disabled
+                                    >
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%= i18n('ssl', 'dns-challenge') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <fieldset class="form-fieldset dns-challenge">
+                                <div class="text-red mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'certbot-warning') %></div>
+
+                                <!-- Certbot DNS plugin selection -->
+                                <div class="row">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group">
+                                            <label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label>
+                                            <select
+                                                    name="meta[dns_provider]"
+                                                    id="dns_provider"
+                                                    class="form-control custom-select"
+                                            >
+                                                <option
+                                                        value=""
+                                                        disabled
+                                                        hidden
+                                                        <%- getDnsProvider() === null ? 'selected' : '' %>
+                                                >Please Choose...</option>
+                                                <% _.each(dns_plugins, function(plugin_info, plugin_name){ %>
+                                                    <option
+                                                            value="<%- plugin_name %>"
+                                                            <%- getDnsProvider() === plugin_name ? 'selected' : '' %>
+                                                    ><%- plugin_info.name %></option>
+                                                <% }); %>
+                                            </select>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- Certbot credentials file content -->
+                                <div class="row credentials-file-content">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group">
+                                            <label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label>
+                                            <textarea
+                                                    name="meta[dns_provider_credentials]"
+                                                    class="form-control text-monospace"
+                                                    id="dns_provider_credentials"
+                                            ><%- getDnsProviderCredentials() %></textarea>
+                                            <div class="text-secondary small">
+                                                <i class="fe fe-info"></i>
+                                                <%= i18n('ssl', 'credentials-file-content-info') %>
+                                            </div>
+                                            <div class="text-red small">
+                                                <i class="fe fe-alert-triangle"></i>
+                                                <%= i18n('ssl', 'stored-as-plaintext-info') %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- DNS propagation delay -->
+                                <div class="row">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group mb-0">
+                                            <label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label>
+                                            <input
+                                                    type="number"
+                                                    min="0"
+                                                    name="meta[propagation_seconds]"
+                                                    class="form-control"
+                                                    id="propagation_seconds"
+                                                    value="<%- getPropagationSeconds() %>"
+                                            >
+                                            <div class="text-secondary small">
+                                                <i class="fe fe-info"></i>
+                                                <%= i18n('ssl', 'propagation-seconds-info') %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </fieldset>
+                        </div>
+
+                        <!-- Lets encrypt -->
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
+                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
+                                </label>
+                            </div>
+                        </div>
                     </div>
                 </div>
-                <div class="col-sm-4 col-md-4">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
-                        <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
-                    </div>
-                </div>
-                <div class="col-sm-6 col-md-6">
-                    <div class="form-group">
-                        <label class="custom-switch">
-                            <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
-                            <span class="custom-switch-indicator"></span>
-                            <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
-                        </label>
-                    </div>
-                </div>
-                <div class="col-sm-6 col-md-6">
-                    <div class="form-group">
-                        <label class="custom-switch">
-                            <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
-                            <span class="custom-switch-indicator"></span>
-                            <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
-                        </label>
-                    </div>
-                </div>
-                <div class="col-sm-12 col-md-12">
-                    <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
-                </div>
             </div>
         </form>
     </div>
diff --git a/frontend/js/app/nginx/stream/form.js b/frontend/js/app/nginx/stream/form.js
index be8fc8bc2..cd012f9b0 100644
--- a/frontend/js/app/nginx/stream/form.js
+++ b/frontend/js/app/nginx/stream/form.js
@@ -1,24 +1,38 @@
-const Mn          = require('backbone.marionette');
-const App         = require('../../main');
-const StreamModel = require('../../../models/stream');
-const template    = require('./form.ejs');
+const Mn            = require('backbone.marionette');
+const App           = require('../../main');
+const StreamModel   = require('../../../models/stream');
+const template      = require('./form.ejs');
+const dns_providers = require('../../../../../global/certbot-dns-plugins');
 
 require('jquery-serializejson');
 require('jquery-mask-plugin');
 require('selectize');
+const Helpers = require("../../../lib/helpers");
+const certListItemTemplate = require("../certificates-list-item.ejs");
+const i18n = require("../../i18n");
 
 module.exports = Mn.View.extend({
     template:  template,
     className: 'modal-dialog',
 
     ui: {
-        form:       'form',
-        forwarding_host: 'input[name="forwarding_host"]',
-        type_error: '.forward-type-error',
-        buttons:    '.modal-footer button',
-        switches:   '.custom-switch-input',
-        cancel:     'button.cancel',
-        save:       'button.save'
+        form:                     'form',
+        forwarding_host:          'input[name="forwarding_host"]',
+        type_error:               '.forward-type-error',
+        buttons:                  '.modal-footer button',
+        switches:                 '.custom-switch-input',
+        cancel:                   'button.cancel',
+        save:                     'button.save',
+        le_error_info:            '#le-error-info',
+        certificate_select:       'select[name="certificate_id"]',
+        domain_names:             'input[name="domain_names"]',
+        dns_challenge_switch:     'input[name="meta[dns_challenge]"]',
+        dns_challenge_content:    '.dns-challenge',
+        dns_provider:             'select[name="meta[dns_provider]"]',
+        credentials_file_content: '.credentials-file-content',
+        dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
+        propagation_seconds:      'input[name="meta[propagation_seconds]"]',
+        letsencrypt:              '.letsencrypt'
     },
 
     events: {
@@ -48,6 +62,35 @@ module.exports = Mn.View.extend({
             data.tcp_forwarding  = !!data.tcp_forwarding;
             data.udp_forwarding  = !!data.udp_forwarding;
 
+            if (typeof data.meta === 'undefined') data.meta = {};
+            data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
+            data.meta.dns_challenge = true;
+
+            if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
+
+            if (typeof data.domain_names === 'string' && data.domain_names) {
+                data.domain_names = data.domain_names.split(',');
+            }
+
+            // Check for any domain names containing wildcards, which are not allowed with letsencrypt
+            if (data.certificate_id === 'new') {
+                let domain_err = false;
+                if (!data.meta.dns_challenge) {
+                    data.domain_names.map(function (name) {
+                        if (name.match(/\*/im)) {
+                            domain_err = true;
+                        }
+                    });
+                }
+
+                if (domain_err) {
+                    alert(i18n('ssl', 'no-wildcard-without-dns'));
+                    return;
+                }
+            } else {
+                data.certificate_id = parseInt(data.certificate_id, 10);
+            }
+
             let method = App.Api.Nginx.Streams.create;
             let is_new = true;
 
@@ -70,10 +113,108 @@ module.exports = Mn.View.extend({
                     });
                 })
                 .catch(err => {
-                    alert(err.message);
+                    let more_info = '';
+                    if (err.code === 500 && err.debug) {
+                        try {
+                            more_info = JSON.parse(err.debug).debug.stack.join("\n");
+                        } catch (e) {
+                        }
+                    }
+                    this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`;
+                    this.ui.le_error_info.show();
+                    this.ui.le_error_info[0].scrollIntoView();
                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
+                    this.ui.save.removeClass('btn-loading');
                 });
-        }
+        },
+
+        'change @ui.certificate_select': function () {
+            let id = this.ui.certificate_select.val();
+            if (id === 'new') {
+                this.ui.letsencrypt.show().find('input').prop('disabled', false);
+                this.ui.domain_names.prop('required', 'required');
+
+                this.ui.dns_challenge_switch
+                    .prop('disabled', true)
+                    .parents('.form-group')
+                    .css('opacity', 0.5);
+
+                this.ui.dns_provider.prop('required', 'required');
+                const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
+                if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
+                    this.ui.dns_provider_credentials.prop('required', 'required');
+                }
+                this.ui.dns_challenge_content.show();
+            } else {
+                this.ui.letsencrypt.hide().find('input').prop('disabled', true);
+            }
+        },
+
+        'change @ui.dns_provider': function () {
+            const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
+            if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
+                this.ui.dns_provider_credentials.prop('required', 'required');
+                this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
+                this.ui.credentials_file_content.show();
+            } else {
+                this.ui.dns_provider_credentials.prop('required', false);
+                this.ui.credentials_file_content.hide();
+            }
+        },
+    },
+
+    templateContext: {
+        getLetsencryptEmail: function () {
+            return App.Cache.User.get('email');
+        },
+        getDnsProvider: function () {
+            return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
+        },
+        getDnsProviderCredentials: function () {
+            return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
+        },
+        getPropagationSeconds: function () {
+            return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
+        },
+        dns_plugins: dns_providers,
+    },
+
+    onRender: function () {
+        let view = this;
+
+        // Certificates
+        this.ui.le_error_info.hide();
+        this.ui.dns_challenge_content.hide();
+        this.ui.credentials_file_content.hide();
+        this.ui.letsencrypt.hide();
+        this.ui.certificate_select.selectize({
+            valueField:       'id',
+            labelField:       'nice_name',
+            searchField:      ['nice_name', 'domain_names'],
+            create:           false,
+            preload:          true,
+            allowEmptyOption: true,
+            render:           {
+                option: function (item) {
+                    item.i18n         = App.i18n;
+                    item.formatDbDate = Helpers.formatDbDate;
+                    return certListItemTemplate(item);
+                }
+            },
+            load:             function (query, callback) {
+                App.Api.Nginx.Certificates.getAll()
+                    .then(rows => {
+                        callback(rows);
+                    })
+                    .catch(err => {
+                        console.error(err);
+                        callback();
+                    });
+            },
+            onLoad:           function () {
+                view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
+            }
+        });
     },
 
     initialize: function (options) {
diff --git a/frontend/js/app/nginx/stream/list/item.ejs b/frontend/js/app/nginx/stream/list/item.ejs
index a8ff83d4c..936247ef8 100644
--- a/frontend/js/app/nginx/stream/list/item.ejs
+++ b/frontend/js/app/nginx/stream/list/item.ejs
@@ -1,6 +1,6 @@
 <td class="text-center">
-    <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>">
-        <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span>
+    <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
+        <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
     </div>
 </td>
 <td>
@@ -16,7 +16,10 @@
 </td>
 <td>
     <div>
-        <% if (tcp_forwarding) { %>
+        <% if (certificate) { %>
+            <span class="tag"><%- i18n('streams', 'tcp+ssl') %></span>
+        <% }
+        else if (tcp_forwarding) { %>
             <span class="tag"><%- i18n('streams', 'tcp') %></span>
         <% }
         if (udp_forwarding) { %>
@@ -24,6 +27,9 @@
         <% } %>
     </div>
 </td>
+<td>
+    <div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div>
+</td>
 <td>
     <%
     var o = isOnline();
diff --git a/frontend/js/app/nginx/stream/list/main.ejs b/frontend/js/app/nginx/stream/list/main.ejs
index 5304f6145..57ab6b2ad 100644
--- a/frontend/js/app/nginx/stream/list/main.ejs
+++ b/frontend/js/app/nginx/stream/list/main.ejs
@@ -3,6 +3,7 @@
     <th><%- i18n('streams', 'incoming-port') %></th>
     <th><%- i18n('str', 'destination') %></th>
     <th><%- i18n('streams', 'protocol') %></th>
+    <th><%- i18n('str', 'ssl') %></th>
     <th><%- i18n('str', 'status') %></th>
     <% if (canManage) { %>
     <th>&nbsp;</th>
diff --git a/frontend/js/app/nginx/stream/main.js b/frontend/js/app/nginx/stream/main.js
index 8a86e5836..83bdc15e1 100644
--- a/frontend/js/app/nginx/stream/main.js
+++ b/frontend/js/app/nginx/stream/main.js
@@ -88,7 +88,7 @@ module.exports = Mn.View.extend({
     onRender: function () {
         let view = this;
 
-        view.fetch(['owner'])
+        view.fetch(['owner', 'certificate'])
             .then(response => {
                 if (!view.isDestroyed()) {
                     if (response && response.length) {
diff --git a/frontend/js/app/user/form.ejs b/frontend/js/app/user/form.ejs
index aeb268f7c..9ba84438a 100644
--- a/frontend/js/app/user/form.ejs
+++ b/frontend/js/app/user/form.ejs
@@ -1,10 +1,10 @@
 <div class="modal-content">
-    <div class="modal-header">
-        <h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5>
-        <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
-    </div>
-    <div class="modal-body">
-        <form>
+    <form>
+        <div class="modal-header">
+            <h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5>
+            <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
+        </div>
+        <div class="modal-body">
             <div class="row">
                 <div class="col-sm-6 col-md-6">
                     <div class="form-group">
@@ -49,10 +49,10 @@
                 </div>
                 <% } %>
             </div>
-        </form>
-    </div>
-    <div class="modal-footer">
-        <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
-        <button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
-    </div>
+        </div>
+        <div class="modal-footer">
+            <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
+            <button type="submit" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
+        </div>
+    </form>
 </div>
diff --git a/frontend/js/app/user/form.js b/frontend/js/app/user/form.js
index ef92ec3e9..617a75fc9 100644
--- a/frontend/js/app/user/form.js
+++ b/frontend/js/app/user/form.js
@@ -19,7 +19,7 @@ module.exports = Mn.View.extend({
 
     events: {
 
-        'click @ui.save': function (e) {
+        'submit @ui.form': function (e) {
             e.preventDefault();
             this.ui.error.hide();
             let view = this;
diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json
index 0bbde4541..a154921b5 100644
--- a/frontend/js/i18n/messages.json
+++ b/frontend/js/i18n/messages.json
@@ -179,7 +179,9 @@
       "delete-confirm": "Are you sure you want to delete this Stream?",
       "help-title": "What is a Stream?",
       "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
-      "search": "Search Incoming Port…"
+      "search": "Search Incoming Port…",
+      "ssl-certificate": "SSL Certificate for TCP Forwarding",
+      "tcp+ssl": "TCP+SSL"
     },
     "certificates": {
       "title": "SSL Certificates",
@@ -206,7 +208,10 @@
       "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
       "download": "Download",
       "renew-title": "Renew Let's Encrypt Certificate",
-      "search": "Search Certificate…"
+      "search": "Search Certificate…",
+      "in-use"  : "In use",
+      "inactive": "Inactive",
+      "active-domain_names": "Active domain names"
     },
     "access-lists": {
       "title": "Access Lists",
diff --git a/frontend/js/models/stream.js b/frontend/js/models/stream.js
index ba035429a..390c4fb05 100644
--- a/frontend/js/models/stream.js
+++ b/frontend/js/models/stream.js
@@ -15,8 +15,11 @@ const model = Backbone.Model.extend({
             udp_forwarding:  false,
             enabled:         true,
             meta:            {},
+            certificate_id:  0,
+            domain_names:    [],
             // The following are expansions:
-            owner:           null
+            owner:           null,
+            certificate:     null
         };
     }
 });
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 371f3c5f9..02655441f 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2648,9 +2648,9 @@ electron-to-chromium@^1.3.47:
   integrity sha512-67V62Z4CFOiAtox+o+tosGfVk0QX4DJgH609tjT8QymbJZVAI/jWnAthnr8c5hnRNziIRwkc9EMQYejiVz3/9Q==
 
 elliptic@^6.5.3, elliptic@^6.5.4:
-  version "6.5.4"
-  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
-  integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
+  version "6.6.0"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.0.tgz#5919ec723286c1edf28685aa89261d4761afa210"
+  integrity sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==
   dependencies:
     bn.js "^4.11.9"
     brorand "^1.1.0"
diff --git a/global/certbot-dns-plugins.json b/global/certbot-dns-plugins.json
index feaa126eb..2191c2138 100644
--- a/global/certbot-dns-plugins.json
+++ b/global/certbot-dns-plugins.json
@@ -7,10 +7,18 @@
 		"credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json",
 		"full_plugin_name": "dns-acmedns"
 	},
+	"active24": {
+		"name": "Active24",
+		"package_name": "certbot-dns-active24",
+		"version": "~=1.5.1",
+		"dependencies": "",
+		"credentials": "dns_active24_token=\"TOKEN\"",
+		"full_plugin_name": "dns-active24"
+	},
 	"aliyun": {
 		"name": "Aliyun",
 		"package_name": "certbot-dns-aliyun",
-		"version": "~=0.38.1",
+		"version": "~=2.0.0",
 		"dependencies": "",
 		"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef",
 		"full_plugin_name": "dns-aliyun"
@@ -23,6 +31,14 @@
 		"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
 		"full_plugin_name": "dns-azure"
 	},
+	"beget": {
+		"name":"Beget",
+		"package_name": "certbot-beget-plugin",
+		"version": "~=1.0.0.dev9",
+		"dependencies": "",
+		"credentials": "# Beget API credentials used by Certbot\nbeget_plugin_username = username\nbeget_plugin_password = password",
+		"full_plugin_name": "beget-plugin"
+	},
 	"bunny": {
 		"name": "bunny.net",
 		"package_name": "certbot-dns-bunny",
@@ -145,11 +161,11 @@
 	},
 	"domainoffensive": {
 		"name": "DomainOffensive (do.de)",
-		"package_name": "certbot-dns-do",
-		"version": "~=0.31.0",
+		"package_name": "certbot-dns-domainoffensive",
+		"version": "~=2.0.0",
 		"dependencies": "",
 		"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN",
-		"full_plugin_name": "dns-do"
+		"full_plugin_name": "dns-domainoffensive"
 	},
 	"domeneshop": {
 		"name": "Domeneshop",
@@ -199,6 +215,14 @@
 		"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
 		"full_plugin_name": "dns-gandi"
 	},
+	"gcore": {
+		"name": "Gcore DNS",
+		"package_name": "certbot-dns-gcore",
+		"version": "~=0.1.8",
+		"dependencies": "",
+		"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
+		"full_plugin_name": "dns-gcore"
+	},
 	"godaddy": {
 		"name": "GoDaddy",
 		"package_name": "certbot-dns-godaddy",
@@ -239,6 +263,14 @@
 		"credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef",
 		"full_plugin_name": "dns-hetzner"
 	},
+	"hostingnl": {
+		"name": "Hosting.nl",
+		"package_name": "certbot-dns-hostingnl",
+		"version": "~=0.1.5",
+		"dependencies": "",
+		"credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef",
+		"full_plugin_name": "dns-hostingnl"
+	},
 	"hover": {
 		"name": "Hover",
 		"package_name": "certbot-dns-hover",
@@ -295,6 +327,14 @@
 		"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
 		"full_plugin_name": "dns-joker"
 	},
+	"leaseweb": {
+		"name": "LeaseWeb",
+		"package_name": "certbot-dns-leaseweb",
+		"version": "~=1.0.1",
+		"dependencies": "",
+		"credentials": "dns_leaseweb_api_token = 01234556789",
+		"full_plugin_name": "dns-leaseweb"
+	},
 	"linode": {
 		"name": "Linode",
 		"package_name": "certbot-dns-linode",
@@ -324,7 +364,7 @@
 		"package_name": "certbot-dns-mijn-host",
 		"version": "~=0.0.4",
 		"dependencies": "",
-		"credentials": "dns-mijn-host-credentials = /etc/letsencrypt/mijnhost-credentials.ini",
+		"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
 		"full_plugin_name": "dns-mijn-host"
 	},
 	"namecheap": {
@@ -386,7 +426,7 @@
 	"porkbun": {
 		"name": "Porkbun",
 		"package_name": "certbot-dns-porkbun",
-		"version": "~=0.2",
+		"version": "~=0.9",
 		"dependencies": "",
 		"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
 		"full_plugin_name": "dns-porkbun"
@@ -415,6 +455,14 @@
 		"credentials": "# Target DNS server\ndns_rfc2136_server = 192.0.2.1\n# Target DNS port\ndns_rfc2136_port = 53\n# TSIG key name\ndns_rfc2136_name = keyname.\n# TSIG key secret\ndns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512",
 		"full_plugin_name": "dns-rfc2136"
 	},
+	"rockenstein": {
+		"name": "rockenstein AG",
+		"package_name": "certbot-dns-rockenstein",
+		"version": "~=1.0.0",
+		"dependencies": "",
+		"credentials": "dns_rockenstein_token=<token>",
+		"full_plugin_name": "dns-rockenstein"
+	},
 	"route53": {
 		"name": "Route 53 (Amazon)",
 		"package_name": "certbot-dns-route53",
@@ -471,12 +519,28 @@
 		"credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>",
 		"full_plugin_name": "dns-websupport"
 	},
-	"wedos":{
+	"wedos": {
 		"name": "Wedos",
 		"package_name": "certbot-dns-wedos",
 		"version": "~=2.2",
 		"dependencies": "",
-		"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_sha256_password>",
+		"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_password>",
 		"full_plugin_name": "dns-wedos"
-	}
+	},
+	"edgedns": {
+		"name": "Akamai Edge DNS",
+		"package_name": "certbot-plugin-edgedns",
+		"version": "~=0.1.0",
+		"dependencies": "",
+		"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf",
+		"full_plugin_name": "edgedns"
+	},
+	"zoneedit": {
+		"name": "ZoneEdit",
+		"package_name": "certbot-dns-zoneedit",
+		"version": "~=0.3.2",
+		"dependencies": "--no-deps dnspython",
+		"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>",
+		"full_plugin_name": "dns-zoneedit"
+ 	}
 }
diff --git a/scripts/.common.sh b/scripts/.common.sh
index 3cea09167..c89dbf4f2 100644
--- a/scripts/.common.sh
+++ b/scripts/.common.sh
@@ -11,7 +11,17 @@ YELLOW='\E[1;33m'
 export BLUE CYAN GREEN RED RESET YELLOW
 
 # Docker Compose
-COMPOSE_PROJECT_NAME="npmdev"
+COMPOSE_PROJECT_NAME="npm2dev"
 COMPOSE_FILE="docker/docker-compose.dev.yml"
 
 export COMPOSE_FILE COMPOSE_PROJECT_NAME
+
+# $1: container_name
+get_container_ip () {
+	local container_name=$1
+	local container
+	local ip
+	container=$(docker-compose ps --all -q "${container_name}" | tail -n1)
+	ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
+	echo "$ip"
+}
diff --git a/scripts/ci/fulltest-cypress b/scripts/ci/fulltest-cypress
index 7e4469fec..c7255ffb2 100755
--- a/scripts/ci/fulltest-cypress
+++ b/scripts/ci/fulltest-cypress
@@ -65,8 +65,10 @@ rm -rf "${LOCAL_RESOLVE}"
 printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
 
 # bring up all remaining containers, except cypress!
-docker-compose up -d --remove-orphans stepca
+docker-compose up -d --remove-orphans stepca squid
 docker-compose pull db-mysql || true # ok to fail
+docker-compose pull db-postgres || true # ok to fail
+docker-compose pull authentik authentik-redis authentik-ldap || true # ok to fail
 docker-compose up -d --remove-orphans --pull=never fullstack
 
 # wait for main container to be healthy
diff --git a/scripts/test-dev b/scripts/cypress-dev
similarity index 68%
rename from scripts/test-dev
rename to scripts/cypress-dev
index f75527b7e..a0c64ad96 100755
--- a/scripts/test-dev
+++ b/scripts/cypress-dev
@@ -6,8 +6,8 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 # Ensure docker-compose exists
 if hash docker-compose 2>/dev/null; then
 	cd "${DIR}/.."
-	echo -e "${BLUE}❯ ${CYAN}Testing Dev Stack ...${RESET}"
-	docker-compose exec -T npm bash -c "cd /app && task test"
+	rm -rf "$DIR/../test/results"
+	docker-compose up --build cypress
 else
 	echo -e "${RED}❯ docker-compose command is not available${RESET}"
 fi
diff --git a/scripts/start-dev b/scripts/start-dev
index f064a4bdf..b972ab805 100755
--- a/scripts/start-dev
+++ b/scripts/start-dev
@@ -7,8 +7,42 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 if hash docker-compose 2>/dev/null; then
 	cd "${DIR}/.."
 	echo -e "${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}"
+	echo -e "${BLUE}❯ $(docker-compose config)${RESET}"
 
-	docker-compose up -d --remove-orphans --force-recreate --build
+	# Bring up a stack, in steps so we can inject IPs everywhere
+	docker-compose up -d pdns pdns-db
+	PDNS_IP=$(get_container_ip "pdns")
+	echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}"
+
+	# adjust the dnsrouter config
+	LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json"
+	rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp"
+	# IMPORTANT: changes to dnsrouter-config.json will affect this line:
+	jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp"
+
+	docker-compose up -d dnsrouter
+	DNSROUTER_IP=$(get_container_ip "dnsrouter")
+	echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}"
+
+	if [ "${DNSROUTER_IP:-}" = "" ]; then
+		echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}"
+		exit 1
+	fi
+
+	# mount the resolver
+	LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf"
+	rm -rf "${LOCAL_RESOLVE}"
+	printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
+
+	# bring up all remaining containers, except cypress!
+	docker-compose up -d --remove-orphans stepca squid
+	docker-compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap
+	docker-compose build --pull --parallel fullstack
+	docker-compose up -d --remove-orphans fullstack
+	docker-compose up -d --remove-orphans swagger
+
+	# wait for main container to be healthy
+	bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120
 
 	echo ""
 	echo -e "${CYAN}Admin UI:     http://127.0.0.1:3081${RESET}"
@@ -18,10 +52,10 @@ if hash docker-compose 2>/dev/null; then
 
 	if [ "$1" == "-f" ]; then
 		echo -e "${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}"
-		docker logs -f npm_core
+		docker logs -f npm2dev.core
 	else
 		echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:"
-		echo "  docker logs -f npm_core"
+		echo "  docker logs -f npm2dev.core"
 	fi
 else
 	echo -e "${RED}❯ docker-compose command is not available${RESET}"
diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile
index 6de4c2454..9b835fe0e 100644
--- a/test/cypress/Dockerfile
+++ b/test/cypress/Dockerfile
@@ -1,11 +1,22 @@
-FROM cypress/included:13.9.0
-
-COPY --chown=1000 ./test /test
+FROM cypress/included:14.0.1
 
 # Disable Cypress CLI colors
 ENV FORCE_COLOR=0
 ENV NO_COLOR=1
 
+# testssl.sh and mkcert
+RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \
+	&& tar -xzf /tmp/testssl.tgz -C /tmp \
+	&& mv /tmp/testssl.sh-3.2rc4 /testssl \
+	&& rm /tmp/testssl.tgz \
+	&& apt-get update \
+	&& apt-get install -y bsdmainutils curl dnsutils \
+	&& apt-get clean \
+	&& rm -rf /var/lib/apt/lists/* \
+	&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \
+	&& chmod +x /bin/mkcert
+
+COPY --chown=1000 ./test /test
 WORKDIR /test
 RUN yarn install && yarn cache clean
 ENTRYPOINT []
diff --git a/test/cypress/config/ci.js b/test/cypress/config/ci.js
index 2b50db1e8..dc968dbd3 100644
--- a/test/cypress/config/ci.js
+++ b/test/cypress/config/ci.js
@@ -15,8 +15,8 @@ module.exports = defineConfig({
 			return require("../plugins/index.js")(on, config);
 		},
 		env: {
-			swaggerBase: '{{baseUrl}}/api/schema',
+			swaggerBase: '{{baseUrl}}/api/schema?ts=' + Date.now(),
 		},
-		baseUrl: 'http://localhost:1234',
+		baseUrl: 'http://fullstack:81',
 	}
 });
diff --git a/test/cypress/config/dev.js b/test/cypress/config/dev.js
deleted file mode 100644
index 90ae943bc..000000000
--- a/test/cypress/config/dev.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const { defineConfig } = require('cypress');
-
-module.exports = defineConfig({
-	requestTimeout: 30000,
-	defaultCommandTimeout: 20000,
-	reporter: 'cypress-multi-reporters',
-	reporterOptions: {
-		configFile: 'multi-reporter.json'
-	},
-	video: false,
-	videosFolder: 'results/videos',
-	screenshotsFolder: 'results/screenshots',
-	e2e: {
-		setupNodeEvents(on, config) {
-			return require("../plugins/index.js")(on, config);
-		},
-		env: {
-			swaggerBase: '{{baseUrl}}/api/schema',
-		},
-		baseUrl: 'http://localhost:1234',
-	}
-});
diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js
new file mode 100644
index 000000000..1e8a6fed4
--- /dev/null
+++ b/test/cypress/e2e/api/Certificates.cy.js
@@ -0,0 +1,99 @@
+/// <reference types="cypress" />
+
+describe('Certificates endpoints', () => {
+	let token;
+	let certID;
+
+	before(() => {
+		cy.getToken().then((tok) => {
+			token = tok;
+		});
+	});
+
+	it('Validate custom certificate', function() {
+		cy.task('backendApiPostFiles', {
+			token: token,
+			path:  '/api/nginx/certificates/validate',
+			files:  {
+				certificate: 'test.example.com.pem',
+				certificate_key: 'test.example.com-key.pem',
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data);
+			expect(data).to.have.property('certificate');
+			expect(data).to.have.property('certificate_key');
+		});
+	});
+
+	it('Custom certificate lifecycle', function() {
+		// Create custom cert
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/certificates',
+			data:  {
+				provider: "other",
+				nice_name: "Test Certificate",
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+			expect(data).to.have.property('id');
+			certID = data.id;
+
+			// Upload files
+			cy.task('backendApiPostFiles', {
+				token: token,
+				path:  `/api/nginx/certificates/${certID}/upload`,
+				files:  {
+					certificate: 'test.example.com.pem',
+					certificate_key: 'test.example.com-key.pem',
+				},
+			}).then((data) => {
+				cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
+				expect(data).to.have.property('certificate');
+				expect(data).to.have.property('certificate_key');
+
+				// Get all certs
+				cy.task('backendApiGet', {
+					token: token,
+					path:  '/api/nginx/certificates?expand=owner'
+				}).then((data) => {
+					cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);
+					expect(data.length).to.be.greaterThan(0);
+
+					// Delete cert
+					cy.task('backendApiDelete', {
+						token: token,
+						path:  `/api/nginx/certificates/${certID}`
+					}).then((data) => {
+						cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);
+						expect(data).to.be.equal(true);
+					});
+				});
+			});
+		});
+	});
+
+	it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/certificates',
+			data:  {
+				domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
+				meta:         {
+					dns_challenge:     false,
+					letsencrypt_agree: true,
+					letsencrypt_email: 'admin@example.com',
+				},
+				provider: 'letsencrypt',
+			},
+			returnOnError: true,
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);
+			expect(data).to.have.property('error');
+			expect(data.error).to.have.property('message');
+			expect(data.error).to.have.property('code');
+			expect(data.error.code).to.equal(400);
+			expect(data.error.message).to.contain('data/domain_names/0 must match pattern');
+		});
+	});
+});
diff --git a/test/cypress/e2e/api/Dashboard.cy.js b/test/cypress/e2e/api/Dashboard.cy.js
new file mode 100644
index 000000000..8fbb97155
--- /dev/null
+++ b/test/cypress/e2e/api/Dashboard.cy.js
@@ -0,0 +1,25 @@
+/// <reference types="cypress" />
+
+describe('Dashboard endpoints', () => {
+	let token;
+
+	before(() => {
+		cy.getToken().then((tok) => {
+			token = tok;
+		});
+	});
+
+	it('Should be able to get host counts', function() {
+		cy.task('backendApiGet', {
+			token: token,
+			path:  '/api/reports/hosts'
+		}).then((data) => {
+			cy.validateSwaggerSchema('get', 200, '/reports/hosts', data);
+			expect(data).to.have.property('dead');
+			expect(data).to.have.property('proxy');
+			expect(data).to.have.property('redirection');
+			expect(data).to.have.property('stream');
+		});
+	});
+
+});
diff --git a/test/cypress/e2e/api/FullCertProvision.cy.js b/test/cypress/e2e/api/FullCertProvision.cy.js
new file mode 100644
index 000000000..5ca5692cd
--- /dev/null
+++ b/test/cypress/e2e/api/FullCertProvision.cy.js
@@ -0,0 +1,62 @@
+/// <reference types="cypress" />
+
+describe('Full Certificate Provisions', () => {
+	let token;
+
+	before(() => {
+		cy.getToken().then((tok) => {
+			token = tok;
+		});
+	});
+
+	it('Should be able to create new http certificate', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/certificates',
+			data:  {
+				domain_names: [
+					'website1.example.com'
+				],
+				meta: {
+					letsencrypt_email: 'admin@example.com',
+					letsencrypt_agree: true,
+					dns_challenge: false
+				},
+				provider: 'letsencrypt'
+			}
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.greaterThan(0);
+			expect(data.provider).to.be.equal('letsencrypt');
+		});
+	});
+
+	it('Should be able to create new DNS certificate with Powerdns', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/certificates',
+			data:  {
+				domain_names: [
+					'website2.example.com'
+				],
+				meta: {
+					letsencrypt_email: "admin@example.com",
+					dns_challenge: true,
+					dns_provider: 'powerdns',
+					dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm',
+					letsencrypt_agree: true,
+					propagation_seconds: 5,
+				},
+				provider: 'letsencrypt'
+			}
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.greaterThan(0);
+			expect(data.provider).to.be.equal('letsencrypt');
+			expect(data.meta.dns_provider).to.be.equal('powerdns');
+		});
+	});
+
+});
diff --git a/test/cypress/e2e/api/Health.cy.js b/test/cypress/e2e/api/Health.cy.js
index 5538916cb..49881e97b 100644
--- a/test/cypress/e2e/api/Health.cy.js
+++ b/test/cypress/e2e/api/Health.cy.js
@@ -1,4 +1,4 @@
-/// <reference types="Cypress" />
+/// <reference types="cypress" />
 
 describe('Basic API checks', () => {
 	it('Should return a valid health payload', function () {
@@ -12,9 +12,9 @@ describe('Basic API checks', () => {
 
 	it('Should return a valid schema payload', function () {
 		cy.task('backendApiGet', {
-			path: '/api/schema',
+			path: '/api/schema?ts=' + Date.now(),
 		}).then((data) => {
-			expect(data.openapi).to.be.equal('3.0.0');
+			expect(data.openapi).to.be.equal('3.1.0');
 		});
 	});
 });
diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js
new file mode 100644
index 000000000..6b7e5f76c
--- /dev/null
+++ b/test/cypress/e2e/api/Ldap.cy.js
@@ -0,0 +1,64 @@
+/// <reference types="cypress" />
+
+describe('LDAP with Authentik', () => {
+	let token;
+	if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
+
+		before(() => {
+			cy.getToken().then((tok) => {
+				token = tok;
+
+				// cy.task('backendApiPut', {
+				// 	token: token,
+				// 	path:  '/api/settings/ldap-auth',
+				// 	data:  {
+				// 		value: {
+				// 			host: 'authentik-ldap:3389',
+				// 			base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io',
+				// 			user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io',
+				// 			email_property: 'mail',
+				// 			name_property: 'sn',
+				// 			self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))',
+				// 			auto_create_user: true
+				// 		}
+				// 	}
+				// }).then((data) => {
+				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
+				// 	expect(data.result).to.have.property('id');
+				// 	expect(data.result.id).to.be.greaterThan(0);
+				// });
+
+				// cy.task('backendApiPut', {
+				// 	token: token,
+				// 	path:  '/api/settings/auth-methods',
+				// 	data:  {
+				// 		value: [
+				// 			'local',
+				// 			'ldap'
+				// 		]
+				// 	}
+				// }).then((data) => {
+				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
+				// 	expect(data.result).to.have.property('id');
+				// 	expect(data.result.id).to.be.greaterThan(0);
+				// });
+			});
+		});
+
+		it.skip('Should log in with LDAP', function() {
+			// cy.task('backendApiPost', {
+			// 	token: token,
+			// 	path:  '/api/auth',
+			// 	data:  {
+			// 		// Authentik LDAP creds:
+			// 		type: 'ldap',
+			// 		identity: 'cypress',
+			// 		secret: 'fqXBfUYqHvYqiwBHWW7f'
+			// 	}
+			// }).then((data) => {
+			// 	cy.validateSwaggerSchema('post', 200, '/auth', data);
+			// 	expect(data.result).to.have.property('token');
+			// });
+		});
+	}
+});
diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js
new file mode 100644
index 000000000..044bb2757
--- /dev/null
+++ b/test/cypress/e2e/api/OAuth.cy.js
@@ -0,0 +1,97 @@
+/// <reference types="cypress" />
+
+describe('OAuth with Authentik', () => {
+	let token;
+	if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
+
+		before(() => {
+			cy.getToken().then((tok) => {
+				token = tok;
+
+				// cy.task('backendApiPut', {
+				// 	token: token,
+				// 	path:  '/api/settings/oauth-auth',
+				// 	data:  {
+				// 		value: {
+				// 			client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU',
+				// 			client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq',
+				// 			authorization_url: 'http://authentik:9000/application/o/authorize/',
+				// 			resource_url: 'http://authentik:9000/application/o/userinfo/',
+				// 			token_url: 'http://authentik:9000/application/o/token/',
+				// 			logout_url: 'http://authentik:9000/application/o/npm/end-session/',
+				// 			identifier: 'preferred_username',
+				// 			scopes: [],
+				// 			auto_create_user: true
+				// 		}
+				// 	}
+				// }).then((data) => {
+				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
+				// 	expect(data.result).to.have.property('id');
+				// 	expect(data.result.id).to.be.greaterThan(0);
+				// });
+
+				// cy.task('backendApiPut', {
+				// 	token: token,
+				// 	path:  '/api/settings/auth-methods',
+				// 	data:  {
+				// 		value: [
+				// 			'local',
+				// 			'oauth'
+				// 		]
+				// 	}
+				// }).then((data) => {
+				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
+				// 	expect(data.result).to.have.property('id');
+				// 	expect(data.result.id).to.be.greaterThan(0);
+				// });
+			});
+		});
+
+		it.skip('Should log in with OAuth', function() {
+			// cy.task('backendApiGet', {
+			// 	path:  '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')),
+			// }).then((data) => {
+			// 	expect(data).to.have.property('result');
+
+			// 	cy.origin('http://authentik:9000', {args: data.result}, (url) => {
+			// 		cy.visit(url);
+			// 		cy.get('ak-flow-executor')
+			// 		.shadow()
+			// 		.find('ak-stage-identification')
+			// 		.shadow()
+			// 		.find('input[name="uidField"]', { visible: true })
+			// 		.type('cypress');
+
+			// 	cy.get('ak-flow-executor')
+			// 		.shadow()
+			// 		.find('ak-stage-identification')
+			// 		.shadow()
+			// 		.find('button[type="submit"]', { visible: true })
+			// 		.click();
+
+			// 	cy.get('ak-flow-executor')
+			// 		.shadow()
+			// 		.find('ak-stage-password')
+			// 		.shadow()
+			// 		.find('input[name="password"]', { visible: true })
+			// 		.type('fqXBfUYqHvYqiwBHWW7f');
+
+			// 	cy.get('ak-flow-executor')
+			// 		.shadow()
+			// 		.find('ak-stage-password')
+			// 		.shadow()
+			// 		.find('button[type="submit"]', { visible: true })
+			// 		.click();
+			// 	})
+
+			// 	// we should be logged in
+			// 	cy.get('#root p.chakra-text')
+			// 		.first()
+			// 		.should('have.text', 'Nginx Proxy Manager');
+
+			// 	// logout:
+			// 	cy.clearLocalStorage();
+			// });
+		});
+	}
+});
diff --git a/test/cypress/e2e/api/Hosts.cy.js b/test/cypress/e2e/api/ProxyHosts.cy.js
similarity index 90%
rename from test/cypress/e2e/api/Hosts.cy.js
rename to test/cypress/e2e/api/ProxyHosts.cy.js
index 4652c8e08..5bc645800 100644
--- a/test/cypress/e2e/api/Hosts.cy.js
+++ b/test/cypress/e2e/api/ProxyHosts.cy.js
@@ -1,6 +1,6 @@
-/// <reference types="Cypress" />
+/// <reference types="cypress" />
 
-describe('Hosts endpoints', () => {
+describe('Proxy Hosts endpoints', () => {
 	let token;
 
 	before(() => {
@@ -39,7 +39,7 @@ describe('Hosts endpoints', () => {
 			expect(data).to.have.property('id');
 			expect(data.id).to.be.greaterThan(0);
 			expect(data).to.have.property('enabled');
-			expect(data.enabled).to.be.greaterThan(0);
+			expect(data).to.have.property("enabled", true);
 			expect(data).to.have.property('meta');
 			expect(typeof data.meta.nginx_online).to.be.equal('undefined');
 		});
diff --git a/test/cypress/e2e/api/Settings.cy.js b/test/cypress/e2e/api/Settings.cy.js
new file mode 100644
index 000000000..6942760c7
--- /dev/null
+++ b/test/cypress/e2e/api/Settings.cy.js
@@ -0,0 +1,124 @@
+/// <reference types="cypress" />
+
+describe('Settings endpoints', () => {
+	let token;
+
+	before(() => {
+		cy.getToken().then((tok) => {
+			token = tok;
+		});
+	});
+
+	it('Get all settings', function() {
+		cy.task('backendApiGet', {
+			token: token,
+			path:  '/api/settings',
+		}).then((data) => {
+			cy.validateSwaggerSchema('get', 200, '/settings', data);
+			expect(data.length).to.be.greaterThan(0);
+		});
+	});
+
+	it('Get default-site setting', function() {
+		cy.task('backendApiGet', {
+			token: token,
+			path:  '/api/settings/default-site',
+		}).then((data) => {
+			cy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+		});
+	});
+
+	it('Default Site congratulations', function() {
+		cy.task('backendApiPut', {
+			token: token,
+			path:  '/api/settings/default-site',
+			data: {
+				value: 'congratulations',
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+			expect(data).to.have.property('value');
+			expect(data.value).to.be.equal('congratulations');
+		});
+	});
+
+	it('Default Site 404', function() {
+		cy.task('backendApiPut', {
+			token: token,
+			path:  '/api/settings/default-site',
+			data: {
+				value: '404',
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+			expect(data).to.have.property('value');
+			expect(data.value).to.be.equal('404');
+		});
+	});
+
+	it('Default Site 444', function() {
+		cy.task('backendApiPut', {
+			token: token,
+			path:  '/api/settings/default-site',
+			data: {
+				value: '444',
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+			expect(data).to.have.property('value');
+			expect(data.value).to.be.equal('444');
+		});
+	});
+
+	it('Default Site redirect', function() {
+		cy.task('backendApiPut', {
+			token: token,
+			path:  '/api/settings/default-site',
+			data: {
+				value: 'redirect',
+				meta: {
+					redirect: 'https://www.google.com',
+				},
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+			expect(data).to.have.property('value');
+			expect(data.value).to.be.equal('redirect');
+			expect(data).to.have.property('meta');
+			expect(data.meta).to.have.property('redirect');
+			expect(data.meta.redirect).to.be.equal('https://www.google.com');
+		});
+	});
+
+	it('Default Site html', function() {
+		cy.task('backendApiPut', {
+			token: token,
+			path:  '/api/settings/default-site',
+			data: {
+				value: 'html',
+				meta: {
+					html: '<p>hello world</p>'
+				},
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.equal('default-site');
+			expect(data).to.have.property('value');
+			expect(data.value).to.be.equal('html');
+			expect(data).to.have.property('meta');
+			expect(data.meta).to.have.property('html');
+			expect(data.meta.html).to.be.equal('<p>hello world</p>');
+		});
+	});
+});
diff --git a/test/cypress/e2e/api/Streams.cy.js b/test/cypress/e2e/api/Streams.cy.js
new file mode 100644
index 000000000..9de43157e
--- /dev/null
+++ b/test/cypress/e2e/api/Streams.cy.js
@@ -0,0 +1,213 @@
+/// <reference types="cypress" />
+
+describe('Streams', () => {
+	let token;
+
+	before(() => {
+		cy.getToken().then((tok) => {
+			token = tok;
+			// Set default site content
+			cy.task('backendApiPut', {
+				token: token,
+				path:  '/api/settings/default-site',
+				data: {
+					value: 'html',
+					meta: {
+						html: '<p>yay it works</p>'
+					},
+				},
+			}).then((data) => {
+				cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+			});
+		});
+
+		// Create a custom cert pair
+		cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => {
+			expect(result.code).to.eq(0);
+			// Install CA
+			cy.exec('mkcert -install').then((result) => {
+				expect(result.code).to.eq(0);
+			});
+		});
+
+		cy.exec('rm -f /test/results/testssl.json');
+	});
+
+	it('Should be able to create TCP Stream', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/streams',
+			data:  {
+				incoming_port: 1500,
+				forwarding_host: '127.0.0.1',
+				forwarding_port: 80,
+				certificate_id: 0,
+				meta: {
+					dns_provider_credentials: "",
+					letsencrypt_agree: false,
+					dns_challenge: true
+				},
+				tcp_forwarding: true,
+				udp_forwarding: false
+			}
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.greaterThan(0);
+			expect(data).to.have.property('enabled', true);
+			expect(data).to.have.property('tcp_forwarding', true);
+			expect(data).to.have.property('udp_forwarding', false);
+
+			cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => {
+				expect(result.code).to.eq(0);
+				expect(result.stdout).to.contain('yay it works');
+			});
+		});
+	});
+
+	it('Should be able to create UDP Stream', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/streams',
+			data:  {
+				incoming_port: 1501,
+				forwarding_host: '127.0.0.1',
+				forwarding_port: 80,
+				certificate_id: 0,
+				meta: {
+					dns_provider_credentials: "",
+					letsencrypt_agree: false,
+					dns_challenge: true
+				},
+				tcp_forwarding: false,
+				udp_forwarding: true
+			}
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.greaterThan(0);
+			expect(data).to.have.property('enabled', true);
+			expect(data).to.have.property('tcp_forwarding', false);
+			expect(data).to.have.property('udp_forwarding', true);
+		});
+	});
+
+	it('Should be able to create TCP/UDP Stream', function() {
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/streams',
+			data:  {
+				incoming_port: 1502,
+				forwarding_host: '127.0.0.1',
+				forwarding_port: 80,
+				certificate_id: 0,
+				meta: {
+					dns_provider_credentials: "",
+					letsencrypt_agree: false,
+					dns_challenge: true
+				},
+				tcp_forwarding: true,
+				udp_forwarding: true
+			}
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
+			expect(data).to.have.property('id');
+			expect(data.id).to.be.greaterThan(0);
+			expect(data).to.have.property('enabled', true);
+			expect(data).to.have.property('tcp_forwarding', true);
+			expect(data).to.have.property('udp_forwarding', true);
+
+			cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => {
+				expect(result.code).to.eq(0);
+				expect(result.stdout).to.contain('yay it works');
+			});
+		});
+	});
+
+	it('Should be able to create SSL TCP Stream', function() {
+		let certID = 0;
+
+		// Create custom cert
+		cy.task('backendApiPost', {
+			token: token,
+			path:  '/api/nginx/certificates',
+			data:  {
+				provider: "other",
+				nice_name: "Custom Certificate for SSL Stream",
+			},
+		}).then((data) => {
+			cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+			expect(data).to.have.property('id');
+			certID = data.id;
+
+			// Upload files
+			cy.task('backendApiPostFiles', {
+				token: token,
+				path:  `/api/nginx/certificates/${certID}/upload`,
+				files:  {
+					certificate: 'website1.pem',
+					certificate_key: 'website1.key.pem',
+				},
+			}).then((data) => {
+				cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
+				expect(data).to.have.property('certificate');
+				expect(data).to.have.property('certificate_key');
+
+				// Create the stream
+				cy.task('backendApiPost', {
+					token: token,
+					path:  '/api/nginx/streams',
+					data:  {
+						incoming_port: 1503,
+						forwarding_host: '127.0.0.1',
+						forwarding_port: 80,
+						certificate_id: certID,
+						meta: {
+							dns_provider_credentials: "",
+							letsencrypt_agree: false,
+							dns_challenge: true
+						},
+						tcp_forwarding: true,
+						udp_forwarding: false
+					}
+				}).then((data) => {
+					cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
+					expect(data).to.have.property('id');
+					expect(data.id).to.be.greaterThan(0);
+					expect(data).to.have.property("enabled", true);
+					expect(data).to.have.property('tcp_forwarding', true);
+					expect(data).to.have.property('udp_forwarding', false);
+					expect(data).to.have.property('certificate_id', certID);
+
+					// Check the ssl termination
+					cy.task('log', '[testssl.sh] Running ...');
+					cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', {
+						timeout: 120000, // 2 minutes
+					}).then((result) => {
+						cy.task('log', '[testssl.sh] ' + result.stdout);
+
+						const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"];
+						const ignoredIDs = [
+							'cert_chain_of_trust',
+							'cert_extlifeSpan',
+							'cert_revocation',
+							'overall_grade',
+						];
+
+						cy.readFile('/test/results/testssl.json').then((data) => {
+							// Parse each array item
+							for (let i = 0; i < data.length; i++) {
+								const item = data[i];
+								if (ignoredIDs.includes(item.id)) {
+									continue;
+								}
+								expect(item.severity).to.be.oneOf(allowedSeverities);
+							}
+						});
+					});
+				});
+			});
+		});
+	});
+
+});
diff --git a/test/cypress/e2e/api/Users.cy.js b/test/cypress/e2e/api/Users.cy.js
index 43303d431..06b183176 100644
--- a/test/cypress/e2e/api/Users.cy.js
+++ b/test/cypress/e2e/api/Users.cy.js
@@ -1,4 +1,4 @@
-/// <reference types="Cypress" />
+/// <reference types="cypress" />
 
 describe('Users endpoints', () => {
 	let token;
diff --git a/test/cypress/fixtures/test.example.com-key.pem b/test/cypress/fixtures/test.example.com-key.pem
new file mode 100644
index 000000000..307cdc307
--- /dev/null
+++ b/test/cypress/fixtures/test.example.com-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd
+qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w
+rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge
+Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ
+oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z
+Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X
+zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU
+ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6
+YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe
+a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu
+W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw
+o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW
+H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+
+N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh
+ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU
+MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31
+qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq
+cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9
+vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO
+utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V
+g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1
+mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq
+YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8
+EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk
+8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM
+RrKmPK/msHKK/sVHiL+NFqo=
+-----END PRIVATE KEY-----
diff --git a/test/cypress/fixtures/test.example.com.pem b/test/cypress/fixtures/test.example.com.pem
new file mode 100644
index 000000000..16340cdfd
--- /dev/null
+++ b/test/cypress/fixtures/test.example.com.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw
+gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1
+cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD
+DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu
+b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe
+bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93
+QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu
+GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+
+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU
+QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB
+Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE
+OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G
+A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB
+/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t
+MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy
+l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s
+VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn
+ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt
+IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u
+m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV
+XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp
+1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw
+hp8bJUp/QN7pnOVCDbjTQ+HVMXw=
+-----END CERTIFICATE-----
diff --git a/test/cypress/plugins/backendApi/client.js b/test/cypress/plugins/backendApi/client.js
index 29684cfdc..6f5f7661e 100644
--- a/test/cypress/plugins/backendApi/client.js
+++ b/test/cypress/plugins/backendApi/client.js
@@ -1,9 +1,14 @@
 const logger  = require('./logger');
-const restler = require('@jc21/restler');
+const axios = require('axios').default;
 
 const BackendApi = function(config, token) {
 	this.config = config;
 	this.token  = token;
+
+	this.axios = axios.create({
+		baseURL: config.baseUrl,
+		timeout: 90000,
+	});
 };
 
 /**
@@ -14,128 +19,113 @@ BackendApi.prototype.setToken = function(token) {
 };
 
 /**
- * @param {string} path
- * @param {bool}   [returnOnError]
- * @returns {Promise<object>}
+ * @param {bool} returnOnError
  */
-BackendApi.prototype.get = function(path, returnOnError) {
-	return new Promise((resolve, reject) => {
-		let headers = {
+BackendApi.prototype._prepareOptions = function(returnOnError) {
+	let options = {
+		headers: {
 			Accept: 'application/json'
-		};
-		if (this.token) {
-			headers.Authorization = 'Bearer ' + this.token;
 		}
-
-		logger('GET ', this.config.baseUrl + path);
-
-		restler
-			.get(this.config.baseUrl + path, {
-				headers: headers,
-			})
-			.on('complete', function(data, response) {
-				logger('Response data:', data);
-				if (!returnOnError && data instanceof Error) {
-					reject(data);
-				} else if (!returnOnError && response.statusCode != 200) {
-					if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
-						reject(new Error(data.error.code + ': ' + data.error.message));
-					} else {
-						reject(new Error('Error ' + response.statusCode));
-					}
-				} else {
-					resolve(data);
-				}
-			});
-	});
+	}
+	if (this.token) {
+		options.headers.Authorization = 'Bearer ' + this.token;
+	}
+	if (returnOnError) {
+		options.validateStatus = function () {
+			return true;
+		}
+	}
+	return options;
 };
 
 /**
- * @param {string} path
- * @param {bool}   [returnOnError]
- * @returns {Promise<object>}
+ * @param {*} response
+ * @param {function} resolve
+ * @param {function} reject
+ * @param {bool} returnOnError
  */
-BackendApi.prototype.delete = function(path, returnOnError) {
-	return new Promise((resolve, reject) => {
-		let headers = {
-			Accept: 'application/json'
-		};
-		if (this.token) {
-			headers.Authorization = 'Bearer ' + this.token;
+BackendApi.prototype._handleResponse = function(response, resolve, reject, returnOnError) {
+	logger('Response data:', response.data);
+	if (!returnOnError && typeof response.data === 'object' && typeof response.data.error === 'object') {
+		if (typeof response.data === 'object' && typeof response.data.error === 'object' && typeof response.data.error.message !== 'undefined') {
+			reject(new Error(response.data.error.code + ': ' + response.data.error.message));
+		} else {
+			reject(new Error('Error ' + response.status));
 		}
-
-		logger('DELETE ', this.config.baseUrl + path);
-
-		restler
-			.del(this.config.baseUrl + path, {
-				headers: headers,
-			})
-			.on('complete', function(data, response) {
-				logger('Response data:', data);
-				if (!returnOnError && data instanceof Error) {
-					reject(data);
-				} else if (!returnOnError && response.statusCode != 200) {
-					if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
-						reject(new Error(data.error.code + ': ' + data.error.message));
-					} else {
-						reject(new Error('Error ' + response.statusCode));
-					}
-				} else {
-					resolve(data);
-				}
-			});
-	});
+	} else {
+		resolve(response.data);
+	}
 };
 
 /**
- * @param {string} path
- * @param {object} data
- * @param {bool}   [returnOnError]
- * @returns {Promise<object>}
+ * @param {*} err
+ * @param {function} resolve
+ * @param {function} reject
+ * @param {bool} returnOnError
  */
-BackendApi.prototype.postJson = function(path, data, returnOnError) {
-	logger('POST ', this.config.baseUrl + path);
-	return this._putPostJson('postJson', path, data, returnOnError);
+BackendApi.prototype._handleError = function(err, resolve, reject, returnOnError) {
+	logger('Axios Error:', err);
+	if (returnOnError) {
+		resolve(typeof err.response.data !== 'undefined' ? err.response.data : err);
+	} else {
+		reject(err);
+	}
 };
 
 /**
+ * @param {string} method
  * @param {string} path
- * @param {object} data
  * @param {bool}   [returnOnError]
+ * @param {*}      [data]
  * @returns {Promise<object>}
  */
-BackendApi.prototype.putJson = function(path, data, returnOnError) {
-	logger('PUT ', this.config.baseUrl + path);
-	return this._putPostJson('putJson', path, data, returnOnError);
+BackendApi.prototype.request = function (method, path, returnOnError, data) {
+	logger(method.toUpperCase(), path);
+	const options = this._prepareOptions(returnOnError);
+
+	return new Promise((resolve, reject) => {
+		let opts = {
+			method: method,
+			url: path,
+			...options
+		}
+		if (data !== undefined && data !== null) {
+			opts.data = data;
+		}
+
+		this.axios(opts)
+			.then((response) => {
+				this._handleResponse(response, resolve, reject, returnOnError);
+			})
+			.catch((err) => {
+				this._handleError(err, resolve, reject, returnOnError);
+			});
+	});
 };
 
 /**
  * @param {string} path
- * @param {object} data
+ * @param {form}   form
  * @param {bool}   [returnOnError]
  * @returns {Promise<object>}
  */
-BackendApi.prototype._putPostJson = function(fn, path, data, returnOnError) {
+BackendApi.prototype.postForm = function (path, form, returnOnError) {
+	logger('POST', this.config.baseUrl + path);
+	const options = this._prepareOptions(returnOnError);
+
 	return new Promise((resolve, reject) => {
-		restler[fn](this.config.baseUrl + path, data, {
-			headers: {
-				Accept:        'application/json',
-				Authorization: 'Bearer ' + this.token,
-			},
-		}).on('complete', function(data, response) {
-			logger('Response data:', data);
-			if (!returnOnError && data instanceof Error) {
-				reject(data);
-			} else if (!returnOnError && (response.statusCode < 200 || response.statusCode >= 300)) {
-				if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
-					reject(new Error(data.error.code + ': ' + data.error.message));
-				} else {
-					reject(new Error('Error ' + response.statusCode));
-				}
-			} else {
-				resolve(data);
-			}
-		});
+		const opts = {
+			...options,
+			...form.getHeaders(),
+		}
+
+		this.axios.post(path, form, opts)
+			.then((response) => {
+				this._handleResponse(response, resolve, reject, returnOnError);
+			})
+			.catch((err) => {
+				this._handleError(err, resolve, reject, returnOnError);
+			});
 	});
 };
 
diff --git a/test/cypress/plugins/backendApi/logger.js b/test/cypress/plugins/backendApi/logger.js
index 98efa260e..8920b8692 100644
--- a/test/cypress/plugins/backendApi/logger.js
+++ b/test/cypress/plugins/backendApi/logger.js
@@ -1,12 +1,7 @@
 const _ = require("lodash");
-const chalk = require("chalk");
 
 module.exports = function() {
-	var arr = _.values(arguments);
-	arr.unshift(
-		chalk.blue.bold("[") +
-			chalk.yellow.bold("Backend API") +
-			chalk.blue.bold("]"),
-	);
+	let arr = _.values(arguments);
+	arr.unshift('[Backend API]');
 	console.log.apply(null, arr);
 };
diff --git a/test/cypress/plugins/backendApi/task.js b/test/cypress/plugins/backendApi/task.js
index 2f67902d5..ab9704f4a 100644
--- a/test/cypress/plugins/backendApi/task.js
+++ b/test/cypress/plugins/backendApi/task.js
@@ -1,8 +1,9 @@
+const fs     = require('fs');
+const FormData = require('form-data');
 const logger = require('./logger');
 const Client = require('./client');
 
 module.exports = function (config) {
-
 	logger('Client Ready using', config.baseUrl);
 
 	return {
@@ -17,7 +18,7 @@ module.exports = function (config) {
 		backendApiGet: (options) => {
 			const api = new Client(config);
 			api.setToken(options.token);
-			return api.get(options.path, options.returnOnError || false);
+			return api.request('get', options.path, options.returnOnError || false);
 		},
 
 		/**
@@ -31,7 +32,26 @@ module.exports = function (config) {
 		backendApiPost: (options) => {
 			const api = new Client(config);
 			api.setToken(options.token);
-			return api.postJson(options.path, options.data, options.returnOnError || false);
+			return api.request('post', options.path, options.returnOnError || false, options.data);
+		},
+
+		/**
+		 * @param   {object}    options
+		 * @param   {string}    options.token        JWT
+		 * @param   {string}    options.path         API path
+		 * @param   {object}    options.files
+		 * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors
+		 * @returns {string}
+		 */
+		backendApiPostFiles: (options) => {
+			const api = new Client(config);
+			api.setToken(options.token);
+
+			const form = new FormData();
+			for (let [key, value] of Object.entries(options.files)) {
+				form.append(key, fs.createReadStream(config.fixturesFolder + '/' + value));
+			}
+			return api.postForm(options.path, form, options.returnOnError || false);
 		},
 
 		/**
@@ -45,7 +65,7 @@ module.exports = function (config) {
 		backendApiPut: (options) => {
 			const api = new Client(config);
 			api.setToken(options.token);
-			return api.putJson(options.path, options.data, options.returnOnError || false);
+			return api.request('put', options.path, options.returnOnError || false, options.data);
 		},
 
 		/**
@@ -58,7 +78,7 @@ module.exports = function (config) {
 		backendApiDelete: (options) => {
 			const api = new Client(config);
 			api.setToken(options.token);
-			return api.delete(options.path, options.returnOnError || false);
+			return api.request('delete', options.path, options.returnOnError || false);
 		}
 	};
-};
\ No newline at end of file
+};
diff --git a/test/cypress/plugins/index.js b/test/cypress/plugins/index.js
index 8cf6eef6c..2f3e3c5af 100644
--- a/test/cypress/plugins/index.js
+++ b/test/cypress/plugins/index.js
@@ -1,4 +1,4 @@
-const {SwaggerValidation} = require('@jc21/cypress-swagger-validation');
+const { SwaggerValidation } = require('@jc21/cypress-swagger-validation');
 
 module.exports = (on, config) => {
 	// Replace swaggerBase config var wildcard
diff --git a/test/package.json b/test/package.json
index b0f6ba79d..a3a3cad6d 100644
--- a/test/package.json
+++ b/test/package.json
@@ -1,26 +1,26 @@
 {
-	"name": "test",
+	"name": "npm-test",
 	"version": "1.0.0",
 	"description": "",
 	"main": "index.js",
 	"dependencies": {
-		"@jc21/cypress-swagger-validation": "^0.2.6",
-		"@jc21/restler": "^3.4.0",
-		"chalk": "^4.1.0",
-		"cypress": "^13.9.0",
-		"cypress-multi-reporters": "^1.6.4",
-		"cypress-wait-until": "^3.0.1",
-		"eslint": "^9.3.0",
+		"@jc21/cypress-swagger-validation": "^0.3.2",
+		"axios": "^1.7.9",
+		"cypress": "^14.0.1",
+		"cypress-multi-reporters": "^2.0.5",
+		"cypress-wait-until": "^3.0.2",
+		"eslint": "^9.19.0",
 		"eslint-plugin-align-assignments": "^1.1.2",
-		"eslint-plugin-chai-friendly": "^0.7.4",
-		"eslint-plugin-cypress": "^3.2.0",
+		"eslint-plugin-chai-friendly": "^1.0.1",
+		"eslint-plugin-cypress": "^4.1.0",
+		"form-data": "^4.0.1",
 		"lodash": "^4.17.21",
-		"mocha": "^10.4.0",
+		"mocha": "^11.1.0",
 		"mocha-junit-reporter": "^2.2.1"
 	},
 	"scripts": {
-		"cypress": "cypress open --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}",
-		"cypress:headless": "cypress run --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}"
+		"cypress": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/ci.js",
+		"cypress:headless": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/ci.js"
 	},
 	"author": "",
 	"license": "ISC"
diff --git a/test/yarn.lock b/test/yarn.lock
index 943147b01..b79e90e80 100644
--- a/test/yarn.lock
+++ b/test/yarn.lock
@@ -11,14 +11,13 @@
     call-me-maybe "^1.0.1"
     js-yaml "^3.13.1"
 
-"@apidevtools/json-schema-ref-parser@9.0.9":
-  version "9.0.9"
-  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b"
-  integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==
+"@apidevtools/json-schema-ref-parser@^11.7.2":
+  version "11.7.2"
+  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz#cdf3e0aded21492364a70e193b45b7cf4177f031"
+  integrity sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==
   dependencies:
     "@jsdevtools/ono" "^7.1.3"
-    "@types/json-schema" "^7.0.6"
-    call-me-maybe "^1.0.1"
+    "@types/json-schema" "^7.0.15"
     js-yaml "^4.1.0"
 
 "@apidevtools/openapi-schemas@^2.1.0":
@@ -49,10 +48,10 @@
   resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
   integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
 
-"@cypress/request@^3.0.0":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960"
-  integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==
+"@cypress/request@^3.0.4":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.5.tgz#d893a6e68ce2636c085fcd8d7283c3186499ba63"
+  integrity sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==
   dependencies:
     aws-sign2 "~0.7.0"
     aws4 "^1.8.0"
@@ -60,14 +59,14 @@
     combined-stream "~1.0.6"
     extend "~3.0.2"
     forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    http-signature "~1.3.6"
+    form-data "~4.0.0"
+    http-signature "~1.4.0"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
     json-stringify-safe "~5.0.1"
     mime-types "~2.1.19"
     performance-now "^2.1.0"
-    qs "6.10.4"
+    qs "6.13.0"
     safe-buffer "^5.1.2"
     tough-cookie "^4.1.3"
     tunnel-agent "^0.6.0"
@@ -88,10 +87,24 @@
   dependencies:
     eslint-visitor-keys "^3.3.0"
 
-"@eslint-community/regexpp@^4.6.1":
-  version "4.10.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
-  integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
+"@eslint-community/regexpp@^4.11.0":
+  version "4.11.1"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f"
+  integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==
+
+"@eslint/config-array@^0.18.0":
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d"
+  integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==
+  dependencies:
+    "@eslint/object-schema" "^2.1.4"
+    debug "^4.3.1"
+    minimatch "^3.1.2"
+
+"@eslint/core@^0.6.0":
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.6.0.tgz#9930b5ba24c406d67a1760e94cdbac616a6eb674"
+  integrity sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==
 
 "@eslint/eslintrc@^3.1.0":
   version "3.1.0"
@@ -108,91 +121,82 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@9.3.0":
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.3.0.tgz#2e8f65c9c55227abc4845b1513c69c32c679d8fe"
-  integrity sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==
+"@eslint/js@9.12.0":
+  version "9.12.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.12.0.tgz#69ca3ca9fab9a808ec6d67b8f6edb156cbac91e1"
+  integrity sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==
+
+"@eslint/object-schema@^2.1.4":
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843"
+  integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==
 
-"@humanwhocodes/config-array@^0.13.0":
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
-  integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
+"@eslint/plugin-kit@^0.2.0":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8"
+  integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==
   dependencies:
-    "@humanwhocodes/object-schema" "^2.0.3"
-    debug "^4.3.1"
-    minimatch "^3.0.5"
+    levn "^0.4.1"
+
+"@humanfs/core@^0.19.0":
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.0.tgz#08db7a8c73bb07673d9ebd925f2dad746411fcec"
+  integrity sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==
+
+"@humanfs/node@^0.16.5":
+  version "0.16.5"
+  resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.5.tgz#a9febb7e7ad2aff65890fdc630938f8d20aa84ba"
+  integrity sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==
+  dependencies:
+    "@humanfs/core" "^0.19.0"
+    "@humanwhocodes/retry" "^0.3.0"
 
 "@humanwhocodes/module-importer@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
 
-"@humanwhocodes/object-schema@^2.0.3":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
-  integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
-
 "@humanwhocodes/retry@^0.3.0":
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570"
   integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==
 
-"@jc21/cypress-swagger-validation@^0.2.6":
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.2.6.tgz#8b61f2413fa81cae6f8c2f33ecce5a6ded897030"
-  integrity sha512-8i8poTwi13e4BRKWpvmXFmqvEfQq3Kn9tunWhNYT7IQwiMeUVou+g1yh99QzuI501DBPtL2XEwjNukxTf5GiyQ==
+"@humanwhocodes/retry@^0.3.1":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
+  integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
+
+"@jc21/cypress-swagger-validation@^0.3.1":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.3.1.tgz#1cdd49850a20f876ed62149623f99988264751be"
+  integrity sha512-Vdt1gLfj8p0tJhA42Cfn43XBbsKocNfVCEVSwkn7RmZgWUyRKjqhBBRTVa9cKZTozyg8Co/yhBMsNyjmHFVXtQ==
   dependencies:
+    "@apidevtools/json-schema-ref-parser" "^11.7.2"
     "@apidevtools/swagger-parser" "^10.1.0"
-    ajv "^8.12.0"
+    ajv "^8.17.1"
+    axios "^1.7.7"
     json-schema "^0.4.0"
-    json-schema-ref-parser "^9.0.6"
     jsonpath "^1.1.1"
     lodash "^4.17.21"
     openapi-types "^12.1.3"
-    picocolors "^1.0.0"
-
-"@jc21/restler@^3.4.0":
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/@jc21/restler/-/restler-3.4.0.tgz#cfa214ddb9946a800c6fe472529f72b01e93c763"
-  integrity sha512-P1Nl2ifoQwqtxcqJKYHvxgPfckeIZWbVSYMlNAP+cL2KNk3U5eErPKt4xr5YLIQ+NarFsHMGH8+CBa00FKAGrw==
-  dependencies:
-    iconv-lite "0.2.11"
-    qs "1.2.0"
-    xml2js "0.4.0"
-    yaml "0.2.3"
+    picocolors "^1.1.0"
 
 "@jsdevtools/ono@^7.1.3":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
   integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
 
-"@nodelib/fs.scandir@2.1.5":
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
-  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
-  dependencies:
-    "@nodelib/fs.stat" "2.0.5"
-    run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.5":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
-  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-
-"@nodelib/fs.walk@^1.2.8":
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
-  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
-  dependencies:
-    "@nodelib/fs.scandir" "2.1.5"
-    fastq "^1.6.0"
-
 "@types/color-name@^1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
 
-"@types/json-schema@^7.0.6":
+"@types/estree@^1.0.6":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
+  integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
+
+"@types/json-schema@^7.0.15":
   version "7.0.15"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -229,6 +233,11 @@ acorn@^8.11.3:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
   integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
 
+acorn@^8.12.0:
+  version "8.12.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
+  integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+
 aggregate-error@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -252,7 +261,17 @@ ajv@^6.12.4:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^8.12.0, ajv@^8.6.3:
+ajv@^8.17.1:
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+  integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
+ajv@^8.6.3:
   version "8.13.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91"
   integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==
@@ -262,11 +281,16 @@ ajv@^8.12.0, ajv@^8.6.3:
     require-from-string "^2.0.2"
     uri-js "^4.4.1"
 
-ansi-colors@4.1.1, ansi-colors@^4.1.1:
+ansi-colors@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
   integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
 
+ansi-colors@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+  integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
+
 ansi-escapes@^4.3.0:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -366,6 +390,15 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c"
   integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==
 
+axios@^1.7.7:
+  version "1.7.7"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
+  integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
+  dependencies:
+    follow-redirects "^1.15.6"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -414,13 +447,13 @@ brace-expansion@^2.0.1:
     balanced-match "^1.0.0"
 
 braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
   dependencies:
-    fill-range "^7.0.1"
+    fill-range "^7.1.1"
 
-browser-stdout@1.3.1:
+browser-stdout@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
@@ -443,13 +476,16 @@ cachedir@^2.3.0:
   resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
   integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
 
-call-bind@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
-  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+call-bind@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
+  integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
   dependencies:
-    function-bind "^1.1.1"
-    get-intrinsic "^1.0.2"
+    es-define-property "^1.0.0"
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+    get-intrinsic "^1.2.4"
+    set-function-length "^1.2.1"
 
 call-me-maybe@^1.0.1:
   version "1.0.1"
@@ -489,10 +525,10 @@ check-more-types@^2.24.0:
   resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
   integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
 
-chokidar@3.5.3:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
-  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+chokidar@^3.5.3:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
+  integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
   dependencies:
     anymatch "~3.1.2"
     braces "~3.0.2"
@@ -564,7 +600,7 @@ colorette@^2.0.16:
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
   integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
 
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+combined-stream@^1.0.8, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -592,9 +628,9 @@ core-util-is@1.0.2:
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
 cross-spawn@^7.0.0, cross-spawn@^7.0.2:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
-  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"
@@ -613,17 +649,17 @@ cypress-multi-reporters@^1.6.4:
     debug "^4.3.4"
     lodash "^4.17.21"
 
-cypress-wait-until@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.1.tgz#6a697a600f4fb8cd2897489a15fda77c9857abec"
-  integrity sha512-kpoa8yL6Bi/JNsThGBbrrm7g4SNzYyBUv9M5pF6/NTVm/ClY0HnJzeuWnHiAUZKIZ5l86Oedb12wQyjx7/CWPg==
+cypress-wait-until@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz#c90dddfa4c46a2c422f5b91d486531c560bae46e"
+  integrity sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w==
 
-cypress@^13.9.0:
-  version "13.9.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.9.0.tgz#b529cfa8f8c39ba163ed0501a25bb5b09c143652"
-  integrity sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==
+cypress@^13.15.0:
+  version "13.15.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.15.0.tgz#5eca5387ef34b2e611cfa291967c69c2cd39381d"
+  integrity sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==
   dependencies:
-    "@cypress/request" "^3.0.0"
+    "@cypress/request" "^3.0.4"
     "@cypress/xvfb" "^1.2.4"
     "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
@@ -662,7 +698,7 @@ cypress@^13.9.0:
     request-progress "^3.0.0"
     semver "^7.5.3"
     supports-color "^8.1.1"
-    tmp "~0.2.1"
+    tmp "~0.2.3"
     untildify "^4.0.0"
     yauzl "^2.10.0"
 
@@ -678,13 +714,6 @@ dayjs@^1.10.4:
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
   integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
 
-debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
 debug@^3.1.0:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -699,6 +728,20 @@ debug@^4.1.1:
   dependencies:
     ms "^2.1.1"
 
+debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+debug@^4.3.5:
+  version "4.3.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
+  integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+  dependencies:
+    ms "^2.1.3"
+
 decamelize@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
@@ -709,15 +752,24 @@ deep-is@^0.1.3, deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
+define-data-property@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+  integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+  dependencies:
+    es-define-property "^1.0.0"
+    es-errors "^1.3.0"
+    gopd "^1.0.1"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-diff@5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
-  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+diff@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
+  integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
 
 ecc-jsbn@~0.1.1:
   version "0.1.2"
@@ -746,21 +798,33 @@ enquirer@^2.3.6:
   dependencies:
     ansi-colors "^4.1.1"
 
+es-define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
+  integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
+  dependencies:
+    get-intrinsic "^1.2.4"
+
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
 escalade@^3.1.1:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
   integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
 
-escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
-  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
 escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
 escodegen@^1.8.1:
   version "1.12.0"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
@@ -778,22 +842,22 @@ eslint-plugin-align-assignments@^1.1.2:
   resolved "https://registry.yarnpkg.com/eslint-plugin-align-assignments/-/eslint-plugin-align-assignments-1.1.2.tgz#83e1a8a826d4adf29e82b52d0bb39c88b301b576"
   integrity sha512-I1ZJgk9EjHfGVU9M2Ex8UkVkkjLL5Y9BS6VNnQHq79eHj2H4/Cgxf36lQSUTLgm2ntB03A2NtF+zg9fyi5vChg==
 
-eslint-plugin-chai-friendly@^0.7.4:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.7.4.tgz#eaf222b848673ef8a00b8e507f7c6fd83d036bf2"
-  integrity sha512-PGPjJ8diYgX1mjLxGJqRop2rrGwZRKImoEOwUOgoIhg0p80MkTaqvmFLe5TF7/iagZHggasvIfQlUyHIhK/PYg==
+eslint-plugin-chai-friendly@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.0.1.tgz#c3290b5294c1145934cf9c07eaa4cec87921d18c"
+  integrity sha512-dxD/uz1YKJ8U4yah1i+V/p/u+kHRy3YxTPe2nJGqb5lCR+ucan/KIexfZ5+q4X+tkllyMe86EBbAkdlwxNy3oQ==
 
-eslint-plugin-cypress@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-3.2.0.tgz#fe2dd9c99ed5dfed5c7be658801e75ed3d9c2265"
-  integrity sha512-HaxMz6BoU4ay+K4WrG9ZJC1NdX06FqSlAwtRDStjM0ORFT7zCNPNuRJ+kUPc17Rt2AMUBSqeD9L0zTR3uZhPpw==
+eslint-plugin-cypress@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-3.5.0.tgz#380ef5049ad80ebeca923db69e4aa96e72fcd893"
+  integrity sha512-JZQ6XnBTNI8h1B9M7wJSFzc48SYbh7VMMKaNTQOFa3BQlnmXPrVc4PKen8R+fpv6VleiPeej6VxloGb42zdRvw==
   dependencies:
     globals "^13.20.0"
 
-eslint-scope@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc"
-  integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==
+eslint-scope@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.1.0.tgz#70214a174d4cbffbc3e8a26911d8bf51b9ae9d30"
+  integrity sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
@@ -808,28 +872,37 @@ eslint-visitor-keys@^4.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
   integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
 
-eslint@^9.3.0:
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.3.0.tgz#36a96db84592618d6ed9074d677e92f4e58c08b9"
-  integrity sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==
+eslint-visitor-keys@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c"
+  integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==
+
+eslint@^9.12.0:
+  version "9.12.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.12.0.tgz#54fcba2876c90528396da0fa44b6446329031e86"
+  integrity sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
-    "@eslint-community/regexpp" "^4.6.1"
+    "@eslint-community/regexpp" "^4.11.0"
+    "@eslint/config-array" "^0.18.0"
+    "@eslint/core" "^0.6.0"
     "@eslint/eslintrc" "^3.1.0"
-    "@eslint/js" "9.3.0"
-    "@humanwhocodes/config-array" "^0.13.0"
+    "@eslint/js" "9.12.0"
+    "@eslint/plugin-kit" "^0.2.0"
+    "@humanfs/node" "^0.16.5"
     "@humanwhocodes/module-importer" "^1.0.1"
-    "@humanwhocodes/retry" "^0.3.0"
-    "@nodelib/fs.walk" "^1.2.8"
+    "@humanwhocodes/retry" "^0.3.1"
+    "@types/estree" "^1.0.6"
+    "@types/json-schema" "^7.0.15"
     ajv "^6.12.4"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^8.0.1"
-    eslint-visitor-keys "^4.0.0"
-    espree "^10.0.1"
-    esquery "^1.4.2"
+    eslint-scope "^8.1.0"
+    eslint-visitor-keys "^4.1.0"
+    espree "^10.2.0"
+    esquery "^1.5.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
     file-entry-cache "^8.0.0"
@@ -838,14 +911,11 @@ eslint@^9.3.0:
     ignore "^5.2.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
-    is-path-inside "^3.0.3"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.4.1"
     lodash.merge "^4.6.2"
     minimatch "^3.1.2"
     natural-compare "^1.4.0"
     optionator "^0.9.3"
-    strip-ansi "^6.0.1"
     text-table "^0.2.0"
 
 espree@^10.0.1:
@@ -857,6 +927,15 @@ espree@^10.0.1:
     acorn-jsx "^5.3.2"
     eslint-visitor-keys "^4.0.0"
 
+espree@^10.2.0:
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6"
+  integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==
+  dependencies:
+    acorn "^8.12.0"
+    acorn-jsx "^5.3.2"
+    eslint-visitor-keys "^4.1.0"
+
 esprima@1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b"
@@ -872,10 +951,10 @@ esprima@^4.0.0:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.4.2:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
-  integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
+esquery@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
+  integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
   dependencies:
     estraverse "^5.1.0"
 
@@ -974,12 +1053,10 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
-fastq@^1.6.0:
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
-  integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
-  dependencies:
-    reusify "^1.0.4"
+fast-uri@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024"
+  integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==
 
 fd-slicer@~1.1.0:
   version "1.1.0"
@@ -1002,14 +1079,14 @@ file-entry-cache@^8.0.0:
   dependencies:
     flat-cache "^4.0.0"
 
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
   dependencies:
     to-regex-range "^5.0.1"
 
-find-up@5.0.0, find-up@^5.0.0:
+find-up@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
   integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
@@ -1035,18 +1112,32 @@ flatted@^3.2.9:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
   integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
 
+follow-redirects@^1.15.6:
+  version "1.15.9"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
+  integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
+
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
-form-data@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
-  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+form-data@^4.0.0, form-data@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
+  integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+form-data@~4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
   dependencies:
     asynckit "^0.4.0"
-    combined-stream "^1.0.6"
+    combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
 fs-extra@^9.1.0:
@@ -1069,24 +1160,26 @@ fsevents@~2.3.2:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
   integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
 
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
 
 get-caller-file@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.0.2:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
-  integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
+  integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
   dependencies:
-    function-bind "^1.1.1"
-    has "^1.0.3"
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+    has-proto "^1.0.1"
     has-symbols "^1.0.3"
+    hasown "^2.0.0"
 
 get-stream@^5.0.0, get-stream@^5.1.0:
   version "5.2.0"
@@ -1123,7 +1216,7 @@ glob-parent@~5.1.2:
   dependencies:
     is-glob "^4.0.1"
 
-glob@8.1.0:
+glob@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
   integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
@@ -1134,18 +1227,6 @@ glob@8.1.0:
     minimatch "^5.0.1"
     once "^1.3.0"
 
-glob@^7.1.3:
-  version "7.1.6"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
-  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 global-dirs@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485"
@@ -1165,6 +1246,13 @@ globals@^14.0.0:
   resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
   integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
 
+gopd@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+  integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+  dependencies:
+    get-intrinsic "^1.1.3"
+
 graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
@@ -1175,42 +1263,49 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
+has-property-descriptors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+  integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+  dependencies:
+    es-define-property "^1.0.0"
+
+has-proto@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
+  integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+
 has-symbols@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
   integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
 
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+hasown@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
   dependencies:
-    function-bind "^1.1.1"
+    function-bind "^1.1.2"
 
-he@1.2.0:
+he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-http-signature@~1.3.6:
-  version "1.3.6"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
-  integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==
+http-signature@~1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f"
+  integrity sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==
   dependencies:
     assert-plus "^1.0.0"
     jsprim "^2.0.2"
-    sshpk "^1.14.1"
+    sshpk "^1.18.0"
 
 human-signals@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
   integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
-iconv-lite@0.2.11:
-  version "0.2.11"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8"
-  integrity sha1-HOYKOleGSiktEyH/RgnKS7llrcg=
-
 ieee754@^1.1.13:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -1313,7 +1408,7 @@ is-number@^7.0.0:
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-path-inside@^3.0.2, is-path-inside@^3.0.3:
+is-path-inside@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
   integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
@@ -1348,13 +1443,6 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
-js-yaml@4.1.0, js-yaml@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
-  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
-  dependencies:
-    argparse "^2.0.1"
-
 js-yaml@^3.13.1:
   version "3.13.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
@@ -1363,6 +1451,13 @@ js-yaml@^3.13.1:
     argparse "^1.0.7"
     esprima "^4.0.0"
 
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
 jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -1373,13 +1468,6 @@ json-buffer@3.0.1:
   resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
   integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
 
-json-schema-ref-parser@^9.0.6:
-  version "9.0.9"
-  resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f"
-  integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==
-  dependencies:
-    "@apidevtools/json-schema-ref-parser" "9.0.9"
-
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -1497,7 +1585,7 @@ lodash@^4.17.21:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-log-symbols@4.1.0, log-symbols@^4.0.0:
+log-symbols@^4.0.0, log-symbols@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
   integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
@@ -1546,28 +1634,14 @@ mimic-fn@^2.1.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-minimatch@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
-  integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
-  dependencies:
-    brace-expansion "^2.0.1"
-
-minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
-  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimatch@^3.0.5, minimatch@^3.1.2:
+minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
   dependencies:
     brace-expansion "^1.1.7"
 
-minimatch@^5.0.1:
+minimatch@^5.0.1, minimatch@^5.1.6:
   version "5.1.6"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
   integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
@@ -1595,38 +1669,38 @@ mocha-junit-reporter@^2.2.1:
     strip-ansi "^6.0.1"
     xml "^1.0.1"
 
-mocha@^10.4.0:
-  version "10.4.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.4.0.tgz#ed03db96ee9cfc6d20c56f8e2af07b961dbae261"
-  integrity sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==
-  dependencies:
-    ansi-colors "4.1.1"
-    browser-stdout "1.3.1"
-    chokidar "3.5.3"
-    debug "4.3.4"
-    diff "5.0.0"
-    escape-string-regexp "4.0.0"
-    find-up "5.0.0"
-    glob "8.1.0"
-    he "1.2.0"
-    js-yaml "4.1.0"
-    log-symbols "4.1.0"
-    minimatch "5.0.1"
-    ms "2.1.3"
-    serialize-javascript "6.0.0"
-    strip-json-comments "3.1.1"
-    supports-color "8.1.1"
-    workerpool "6.2.1"
-    yargs "16.2.0"
-    yargs-parser "20.2.4"
-    yargs-unparser "2.0.0"
+mocha@^10.7.3:
+  version "10.7.3"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752"
+  integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==
+  dependencies:
+    ansi-colors "^4.1.3"
+    browser-stdout "^1.3.1"
+    chokidar "^3.5.3"
+    debug "^4.3.5"
+    diff "^5.2.0"
+    escape-string-regexp "^4.0.0"
+    find-up "^5.0.0"
+    glob "^8.1.0"
+    he "^1.2.0"
+    js-yaml "^4.1.0"
+    log-symbols "^4.1.0"
+    minimatch "^5.1.6"
+    ms "^2.1.3"
+    serialize-javascript "^6.0.2"
+    strip-json-comments "^3.1.1"
+    supports-color "^8.1.1"
+    workerpool "^6.5.1"
+    yargs "^16.2.0"
+    yargs-parser "^20.2.9"
+    yargs-unparser "^2.0.0"
 
 ms@2.1.2, ms@^2.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3:
+ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -1648,10 +1722,10 @@ npm-run-path@^4.0.0:
   dependencies:
     path-key "^3.0.0"
 
-object-inspect@^1.9.0:
-  version "1.12.3"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
-  integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+object-inspect@^1.13.1:
+  version "1.13.2"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
+  integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
 
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
@@ -1734,11 +1808,6 @@ path-exists@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
   integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
 
-path-is-absolute@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
 path-key@^3.0.0, path-key@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -1754,10 +1823,10 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-picocolors@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
-  integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+picocolors@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
+  integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
 
 picomatch@^2.0.4:
   version "2.2.2"
@@ -1799,6 +1868,11 @@ proxy-from-env@1.0.0:
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
   integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 psl@^1.1.33:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
@@ -1817,28 +1891,18 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-qs@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.0.tgz#ed079be28682147e6fd9a34cc2b0c1e0ec6453ee"
-  integrity sha1-7Qeb4oaCFH5v2aNMwrDB4OxkU+4=
-
-qs@6.10.4:
-  version "6.10.4"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7"
-  integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==
+qs@6.13.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+  integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
   dependencies:
-    side-channel "^1.0.4"
+    side-channel "^1.0.6"
 
 querystringify@^2.1.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
   integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
 
-queue-microtask@^1.2.2:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
-  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-
 randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -1888,30 +1952,11 @@ restore-cursor@^3.1.0:
     onetime "^5.1.0"
     signal-exit "^3.0.2"
 
-reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
 rfdc@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
   integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
 
-rimraf@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
-  dependencies:
-    glob "^7.1.3"
-
-run-parallel@^1.1.9:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
-  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
-  dependencies:
-    queue-microtask "^1.2.2"
-
 rxjs@^7.5.1:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
@@ -1934,23 +1979,30 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-sax@0.5.x:
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1"
-  integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=
-
 semver@^7.5.3:
   version "7.6.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
   integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
 
-serialize-javascript@6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
-  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+serialize-javascript@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+  integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
   dependencies:
     randombytes "^2.1.0"
 
+set-function-length@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+  integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+  dependencies:
+    define-data-property "^1.1.4"
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+    get-intrinsic "^1.2.4"
+    gopd "^1.0.1"
+    has-property-descriptors "^1.0.2"
+
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -1963,14 +2015,15 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-side-channel@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
-  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+side-channel@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
+  integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
   dependencies:
-    call-bind "^1.0.0"
-    get-intrinsic "^1.0.2"
-    object-inspect "^1.9.0"
+    call-bind "^1.0.7"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.4"
+    object-inspect "^1.13.1"
 
 signal-exit@^3.0.2:
   version "3.0.2"
@@ -2005,10 +2058,10 @@ sprintf-js@~1.0.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-sshpk@^1.14.1:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
-  integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
+sshpk@^1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
+  integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
@@ -2064,18 +2117,11 @@ strip-final-newline@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@3.1.1, strip-json-comments@^3.1.1:
+strip-json-comments@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-supports-color@8.1.1, supports-color@^8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
-  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
-  dependencies:
-    has-flag "^4.0.0"
-
 supports-color@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
@@ -2083,6 +2129,13 @@ supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-color@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -2098,12 +2151,10 @@ through@^2.3.8:
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
 
-tmp@~0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
-  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
-  dependencies:
-    rimraf "^3.0.0"
+tmp@~0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae"
+  integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==
 
 to-regex-range@^5.0.1:
   version "5.0.1"
@@ -2236,10 +2287,10 @@ word-wrap@~1.2.3:
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f"
   integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==
 
-workerpool@6.2.1:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
-  integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
+workerpool@^6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
+  integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
 
 wrap-ansi@^6.2.0:
   version "6.2.0"
@@ -2264,45 +2315,22 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-xml2js@0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.0.tgz#124fc4114b4129c810800ecb2ac86cf25462cb9a"
-  integrity sha1-Ek/EEUtBKcgQgA7LKshs8lRiy5o=
-  dependencies:
-    sax "0.5.x"
-    xmlbuilder ">=0.4.2"
-
 xml@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
   integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
 
-xmlbuilder@>=0.4.2:
-  version "13.0.2"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7"
-  integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==
-
 y18n@^5.0.5:
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
-yaml@0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-0.2.3.tgz#b5450e92e76ef36b5dd24e3660091ebaeef3e5c7"
-  integrity sha1-tUUOkudu82td0k42YAkeuu7z5cc=
-
-yargs-parser@20.2.4:
-  version "20.2.4"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
-  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
-
-yargs-parser@^20.2.2:
+yargs-parser@^20.2.2, yargs-parser@^20.2.9:
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
   integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
 
-yargs-unparser@2.0.0:
+yargs-unparser@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
   integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
@@ -2312,7 +2340,7 @@ yargs-unparser@2.0.0:
     flat "^5.0.2"
     is-plain-obj "^2.1.0"
 
-yargs@16.2.0:
+yargs@^16.2.0:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
   integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==