[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n<!--\n \nAre you in the right place?\n- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.\n- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.\n- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*\n\n-->\n\n**Checklist**\n- Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image?\n  - Yes / No\n- Are you sure you're not using someone else's docker image?\n  - Yes / No\n- Have you searched for similar issues (both open and closed)?\n  - Yes / No\n\n**Describe the bug**\n<!-- A clear and concise description of what the bug is. -->\n\n\n**Nginx Proxy Manager Version**\n<!-- What version of Nginx Proxy Manager is reported on the login page? -->\n\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n\n**Expected behavior**\n<!-- A clear and concise description of what you expected to happen. -->\n\n\n**Screenshots**\n<!-- If applicable, add screenshots to help explain your problem. -->\n\n\n**Operating System**\n<!-- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. -->\n\n\n**Additional context**\n<!-- Add any other context about the problem here, docker version, browser version, logs if applicable to the problem. Too much info is better than too little. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/dns_challenge_request.md",
    "content": "---\nname: DNS challenge provider request\nabout: Suggest a new provider to be available for a certificate DNS challenge\ntitle: ''\nlabels: dns provider request\nassignees: ''\n\n---\n\n**What provider would you like to see added to NPM?**\n<!-- What is this provider called? -->\n\n\n**Have you checked if a certbot plugin exists?**\n<!-- \nCurrently NPM only supports DNS challenge providers for which a certbot plugin exists. \nYou can visit pypi.org, and search for a package with the name `certbot-dns-<privider>`.\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n<!--\n\nAre you in the right place?\n- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.\n- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.\n- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*\n\n-->\n\n**Is your feature request related to a problem? Please describe.**\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n\n**Describe the solution you'd like**\n<!-- A clear and concise description of what you want to happen. -->\n\n\n**Describe alternatives you've considered**\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n\n**Additional context**\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/backend\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      dev-patch-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"patch\"\n      dev-minor-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"minor\"\n      prod-patch-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"patch\"\n      prod-minor-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"minor\"\n\n  - package-ecosystem: \"npm\"\n    directory: \"/frontend\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      dev-patch-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"patch\"\n      dev-minor-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"minor\"\n      prod-patch-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"patch\"\n      prod-minor-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"minor\"\n\n  - package-ecosystem: \"npm\"\n    directory: \"/docs\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      dev-patch-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"patch\"\n      dev-minor-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"minor\"\n      prod-patch-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"patch\"\n      prod-minor-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"minor\"\n\n  - package-ecosystem: \"npm\"\n    directory: \"/test\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      dev-patch-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"patch\"\n      dev-minor-updates:\n        dependency-type: \"development\"\n        update-types:\n          - \"minor\"\n      prod-patch-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"patch\"\n      prod-minor-updates:\n        dependency-type: \"production\"\n        update-types:\n          - \"minor\"\n\n  - package-ecosystem: \"docker\"\n    directory: \"/docker\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      updates:\n        update-types:\n          - \"patch\"\n          - \"minor\"\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: 'Close stale issues and PRs'\non:\n  schedule:\n    - cron: '30 1 * * *'\n  workflow_dispatch:\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v10\n        with:\n          stale-issue-label: 'stale'\n          stale-pr-label: 'stale'\n          stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:'\n          stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:'\n          close-issue-message: 'Issue was closed due to inactivity.'\n          close-pr-message: 'PR was closed due to inactivity.'\n          days-before-stale: 182\n          days-before-close: 365\n          operations-per-run: 50\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.idea\n.qodo\n._*\n.vscode\ncertbot-help.txt\ntest/node_modules\n*/node_modules\ndocker/dev/dnsrouter-config.json.tmp\ndocker/dev/resolv.conf\n"
  },
  {
    "path": ".version",
    "content": "2.14.0\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<img src=\"https://nginxproxymanager.com/github.png\">\n\t<br><br>\n\t<img src=\"https://img.shields.io/badge/version-2.14.0-green.svg?style=for-the-badge\">\n\t<a href=\"https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager\">\n\t\t<img src=\"https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge\">\n\t</a>\n\t<a href=\"https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager\">\n\t\t<img src=\"https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge\">\n\t</a>\n</p>\n\nThis project comes as a pre-built docker image that enables you to easily forward to your websites\nrunning at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.\n\n- [Quick Setup](#quick-setup)\n- [Full Setup](https://nginxproxymanager.com/setup/)\n- [Screenshots](https://nginxproxymanager.com/screenshots/)\n\n## Project Goal\n\nI created this project to fill a personal need to provide users with an easy way to accomplish reverse\nproxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.\nWhile there might be advanced options they are optional and the project should be as simple as possible\nso that the barrier for entry here is low.\n\n<a href=\"https://www.buymeacoffee.com/jc21\" target=\"_blank\"><img src=\"http://public.jc21.com/github/by-me-a-coffee.png\" alt=\"Buy Me A Coffee\" style=\"height: 51px !important;width: 217px !important;\" ></a>\n\n\n## Features\n\n- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/)\n- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx\n- Free SSL using Let's Encrypt or provide your own custom SSL certificates\n- Access Lists and basic HTTP Authentication for your hosts\n- Advanced Nginx configuration available for super users\n- User management, permissions and audit log\n\n::: warning\n`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please\nuse the `2.13.7` image tag if this applies to you.\n:::\n\n## Hosting your home network\n\nI won't go in to too much detail here but here are the basics for someone new to this self-hosted world.\n\n1. Your home router will have a Port Forwarding section somewhere. Log in and find it\n2. Add port forwarding for port 80 and 443 to the server hosting this project\n3. Configure your domain name details to point to your home, either with a static ip or a service like\n   - DuckDNS\n   - [Amazon Route53](https://github.com/jc21/route53-ddns)\n   - [Cloudflare](https://github.com/jc21/cloudflare-ddns)\n4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services\n\n## Quick Setup\n\n1. [Install Docker](https://docs.docker.com/install/)\n2. Create a docker-compose.yml file similar to this:\n\n```yml\nservices:\n  app:\n    image: 'docker.io/jc21/nginx-proxy-manager:latest'\n    restart: unless-stopped\n    ports:\n      - '80:80'\n      - '81:81'\n      - '443:443'\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n```\n\nThis is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.\n\n3. Bring up your stack by running\n\n```bash\ndocker compose up -d\n```\n\n4. Log in to the Admin UI\n\nWhen your docker container is running, connect to it on port `81` for the admin interface.\nSometimes this can take a little bit because of the entropy of keys.\n\n[http://127.0.0.1:81](http://127.0.0.1:81)\n\n\n## Contributing\n\nAll are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch.\n\nCI is used in this project. All PR's must pass before being considered. After passing,\ndocker builds for PR's are available on dockerhub for manual verifications.\n\nDocumentation within the `develop` branch is available for preview at\n[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com)\n\n\n### Contributors\n\nSpecial thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).\n\n\n## Getting Support\n\n1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)\n2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)\n3. [Reddit](https://reddit.com/r/nginxproxymanager)\n"
  },
  {
    "path": "backend/.gitignore",
    "content": "config/development.json\ndata/*\nyarn-error.log\ntmp\ncertbot.log\nnode_modules\ncore.*\n\n"
  },
  {
    "path": "backend/app.js",
    "content": "import bodyParser from \"body-parser\";\nimport compression from \"compression\";\nimport express from \"express\";\nimport fileUpload from \"express-fileupload\";\nimport { isDebugMode } from \"./lib/config.js\";\nimport cors from \"./lib/express/cors.js\";\nimport jwt from \"./lib/express/jwt.js\";\nimport { debug, express as logger } from \"./logger.js\";\nimport mainRoutes from \"./routes/main.js\";\n\n/**\n * App\n */\nconst app = express();\napp.use(fileUpload());\napp.use(bodyParser.json());\napp.use(bodyParser.urlencoded({ extended: true }));\n\n// Gzip\napp.use(compression());\n\n/**\n * General Logging, BEFORE routes\n */\n\napp.disable(\"x-powered-by\");\napp.enable(\"trust proxy\", [\"loopback\", \"linklocal\", \"uniquelocal\"]);\napp.enable(\"strict routing\");\n\n// pretty print JSON when not live\nif (isDebugMode()) {\n\tapp.set(\"json spaces\", 2);\n}\n\n// CORS for everything\napp.use(cors);\n\n// General security/cache related headers + server header\napp.use((_, res, next) => {\n\tlet x_frame_options = \"DENY\";\n\n\tif (typeof process.env.X_FRAME_OPTIONS !== \"undefined\" && process.env.X_FRAME_OPTIONS) {\n\t\tx_frame_options = process.env.X_FRAME_OPTIONS;\n\t}\n\n\tres.set({\n\t\t\"X-XSS-Protection\": \"1; mode=block\",\n\t\t\"X-Content-Type-Options\": \"nosniff\",\n\t\t\"X-Frame-Options\": x_frame_options,\n\t\t\"Cache-Control\": \"no-cache, no-store, max-age=0, must-revalidate\",\n\t\tPragma: \"no-cache\",\n\t\tExpires: 0,\n\t});\n\tnext();\n});\n\napp.use(jwt());\napp.use(\"/\", mainRoutes);\n\n// production error handler\n// no stacktraces leaked to user\napp.use((err, req, res, _) => {\n\tconst payload = {\n\t\terror: {\n\t\t\tcode: err.status,\n\t\t\tmessage: err.public ? err.message : \"Internal Error\",\n\t\t},\n\t};\n\n\tif (typeof err.message_i18n !== \"undefined\") {\n\t\tpayload.error.message_i18n = err.message_i18n;\n\t}\n\n\tif (isDebugMode() || (req.baseUrl + req.path).includes(\"nginx/certificates\")) {\n\t\tpayload.debug = {\n\t\t\tstack: typeof err.stack !== \"undefined\" && err.stack ? err.stack.split(\"\\n\") : null,\n\t\t\tprevious: err.previous,\n\t\t};\n\t}\n\n\t// Not every error is worth logging - but this is good for now until it gets annoying.\n\tif (typeof err.stack !== \"undefined\" && err.stack) {\n\t\tdebug(logger, err.stack);\n\t\tif (typeof err.public === \"undefined\" || !err.public) {\n\t\t\tlogger.warn(err.message);\n\t\t}\n\t}\n\n\tres.status(err.status || 500).send(payload);\n});\n\nexport default app;\n"
  },
  {
    "path": "backend/biome.json",
    "content": "{\n    \"$schema\": \"https://biomejs.dev/schemas/2.4.5/schema.json\",\n    \"vcs\": {\n        \"enabled\": true,\n        \"clientKind\": \"git\",\n        \"useIgnoreFile\": true\n    },\n    \"files\": {\n        \"ignoreUnknown\": false,\n        \"includes\": [\n            \"**/*.ts\",\n            \"**/*.tsx\",\n            \"**/*.js\",\n            \"**/*.jsx\",\n            \"!**/dist/**/*\"\n        ]\n    },\n    \"formatter\": {\n        \"enabled\": true,\n        \"indentStyle\": \"tab\",\n        \"indentWidth\": 4,\n        \"lineWidth\": 120,\n        \"formatWithErrors\": true\n    },\n    \"assist\": {\n        \"actions\": {\n            \"source\": {\n                \"organizeImports\": {\n                    \"level\": \"on\",\n                    \"options\": {\n                        \"groups\": [\n                            \":BUN:\",\n                            \":NODE:\",\n                            [\n                                \"npm:*\",\n                                \"npm:*/**\"\n                            ],\n                            \":PACKAGE_WITH_PROTOCOL:\",\n                            \":URL:\",\n                            \":PACKAGE:\",\n                            [\n                                \"/src/*\",\n                                \"/src/**\"\n                            ],\n                            [\n                                \"/**\"\n                            ],\n                            [\n                                \"#*\",\n                                \"#*/**\"\n                            ],\n                            \":PATH:\"\n                        ]\n                    }\n                }\n            }\n        }\n    },\n    \"linter\": {\n        \"enabled\": true,\n        \"rules\": {\n            \"recommended\": true,\n            \"correctness\": {\n                \"useUniqueElementIds\": \"off\"\n            },\n            \"suspicious\": {\n                \"noExplicitAny\": \"off\"\n            },\n            \"performance\": {\n                \"noDelete\": \"off\"\n            },\n            \"nursery\": \"off\",\n            \"a11y\": {\n                \"useSemanticElements\": \"off\",\n                \"useValidAnchor\": \"off\"\n            },\n            \"style\": {\n                \"noParameterAssign\": \"error\",\n                \"useAsConstAssertion\": \"error\",\n                \"useDefaultParameterLast\": \"error\",\n                \"useEnumInitializers\": \"error\",\n                \"useSelfClosingElements\": \"error\",\n                \"useSingleVarDeclarator\": \"error\",\n                \"noUnusedTemplateLiteral\": \"error\",\n                \"useNumberNamespace\": \"error\",\n                \"noInferrableTypes\": \"error\",\n                \"noUselessElse\": \"error\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/certbot/README.md",
    "content": "# Certbot dns-plugins\n\nThis file contains info about available Certbot DNS plugins.\nThis only works for plugins which use the standard argument structure, so:\n--authenticator <plugin-name> --<plugin-name>-credentials <FILE> --<plugin-name>-propagation-seconds <number>\n\nFile Structure:\n\n```json\n{\n  \"cloudflare\": {\n    \"display_name\": \"Name displayed to the user\",\n    \"package_name\": \"Package name in PyPi repo\",\n    \"version_requirement\": \"Optional package version requirements (e.g. ==1.3 or >=1.2,<2.0, see https://www.python.org/dev/peps/pep-0440/#version-specifiers)\",\n    \"dependencies\": \"Additional dependencies, space separated (as you would pass it to pip install)\",\n    \"credentials\": \"Template of the credentials file\",\n    \"full_plugin_name\": \"The full plugin name as used in the commandline with certbot, e.g. 'dns-njalla'\"\n  },\n  ...\n}\n```\n"
  },
  {
    "path": "backend/certbot/dns-plugins.json",
    "content": "{\n\t\"acmedns\": {\n\t\t\"name\": \"ACME-DNS\",\n\t\t\"package_name\": \"certbot-dns-acmedns\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_acmedns_api_url = http://acmedns-server/\\ndns_acmedns_registration_file = /data/acme-registration.json\",\n\t\t\"full_plugin_name\": \"dns-acmedns\"\n\t},\n\t\"active24\": {\n\t\t\"name\": \"Active24\",\n\t\t\"package_name\": \"certbot-dns-active24\",\n\t\t\"version\": \"~=2.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_active24_api_key = <identifier>\\ndns_active24_secret = <secret>\",\n\t\t\"full_plugin_name\": \"dns-active24\"\n\t},\n\t\"aliyun\": {\n\t\t\"name\": \"Aliyun\",\n\t\t\"package_name\": \"certbot-dns-aliyun\",\n\t\t\"version\": \"~=2.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_aliyun_access_key = 12345678\\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef\",\n\t\t\"full_plugin_name\": \"dns-aliyun\"\n\t},\n\t\"arvan\": {\n        \"name\": \"ArvanCloud\",\n        \"package_name\": \"certbot-dns-arvan\",\n        \"version\": \">=0.1.0\",\n        \"dependencies\": \"\",\n        \"credentials\": \"dns_arvan_key = Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n        \"full_plugin_name\": \"dns-arvan\"\n    },\n\t\"azure\": {\n\t\t\"name\": \"Azure\",\n\t\t\"package_name\": \"certbot-dns-azure\",\n\t\t\"version\": \"~=2.6.1\",\n\t\t\"dependencies\": \"azure-mgmt-dns==8.2.0\",\n\t\t\"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\",\n\t\t\"full_plugin_name\": \"dns-azure\"\n\t},\n\t\"baidu\": {\n\t\t\"name\": \"baidu\",\n\t\t\"package_name\": \"certbot-dns-baidu\",\n\t\t\"version\": \"~=0.1.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_baidu_access_key = 12345678\\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef\",\n\t\t\"full_plugin_name\": \"dns-baidu\"\n\t},\n\t\"beget\": {\n\t\t\"name\":\"Beget\",\n\t\t\"package_name\": \"certbot-beget-plugin\",\n\t\t\"version\": \"~=1.0.0.dev9\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# Beget API credentials used by Certbot\\nbeget_plugin_username = username\\nbeget_plugin_password = password\",\n\t\t\"full_plugin_name\": \"beget-plugin\"\n\t},\n\t\"bunny\": {\n\t\t\"name\": \"bunny.net\",\n\t\t\"package_name\": \"certbot-dns-bunny\",\n\t\t\"version\": \"~=0.0.9\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx\",\n\t\t\"full_plugin_name\": \"dns-bunny\"\n\t},\n\t\"cdmon\": {\n\t\t\"name\": \"cdmon\",\n\t\t\"package_name\": \"certbot-dns-cdmon\",\n\t\t\"version\": \"~=0.4.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_cdmon_api_key=your-cdmon-api-token\\ndns_cdmon_domain=your_domain_is_optional\",\n\t\t\"full_plugin_name\": \"dns-cdmon\"\n\t},\n\t\"cloudflare\": {\n\t\t\"name\": \"Cloudflare\",\n\t\t\"package_name\": \"certbot-dns-cloudflare\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"# Cloudflare API token\\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567\",\n\t\t\"full_plugin_name\": \"dns-cloudflare\"\n\t},\n\t\"cloudns\": {\n\t\t\"name\": \"ClouDNS\",\n\t\t\"package_name\": \"certbot-dns-cloudns\",\n\t\t\"version\": \"~=0.7.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# Target user ID (see https://www.cloudns.net/api-settings/)\\n\\tdns_cloudns_auth_id=1234\\n\\t# Alternatively, one of the following two options can be set:\\n\\t# dns_cloudns_sub_auth_id=1234\\n\\t# dns_cloudns_sub_auth_user=foobar\\n\\n\\t# API password\\n\\tdns_cloudns_auth_password=password1\",\n\t\t\"full_plugin_name\": \"dns-cloudns\"\n\t},\n\t\"cloudxns\": {\n\t\t\"name\": \"CloudXNS\",\n\t\t\"package_name\": \"certbot-dns-cloudxns\",\n\t\t\"version\": \"~=1.32.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef\\ndns_cloudxns_secret_key = 1122334455667788\",\n\t\t\"full_plugin_name\": \"dns-cloudxns\"\n\t},\n\t\"constellix\": {\n\t\t\"name\": \"Constellix\",\n\t\t\"package_name\": \"certbot-dns-constellix\",\n\t\t\"version\": \"~=0.2.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595\\ndns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b\\ndns_constellix_endpoint = https://api.dns.constellix.com/v1\",\n\t\t\"full_plugin_name\": \"dns-constellix\"\n\t},\n\t\"corenetworks\": {\n\t\t\"name\": \"Core Networks\",\n\t\t\"package_name\": \"certbot-dns-corenetworks\",\n\t\t\"version\": \"~=0.1.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_corenetworks_username = asaHB12r\\ndns_corenetworks_password = secure_password\",\n\t\t\"full_plugin_name\": \"dns-corenetworks\"\n\t},\n\t\"cpanel\": {\n\t\t\"name\": \"cPanel\",\n\t\t\"package_name\": \"certbot-dns-cpanel\",\n\t\t\"version\": \"~=0.4.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"cpanel_url = https://cpanel.example.com:2083\\ncpanel_username = your_username\\ncpanel_password = your_password\\ncpanel_token = your_api_token\",\n\t\t\"full_plugin_name\": \"cpanel\"\n\t},\n\t\"ddnss\": {\n\t\t\"name\": \"DDNSS\",\n\t\t\"package_name\": \"certbot-dns-ddnss\",\n\t\t\"version\": \"~=1.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_ddnss_token = YOUR_DDNSS_API_TOKEN\",\n\t\t\"full_plugin_name\": \"dns-ddnss\"\n\t},\n\t\"desec\": {\n\t\t\"name\": \"deSEC\",\n\t\t\"package_name\": \"certbot-dns-desec\",\n\t\t\"version\": \"~=1.2.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_desec_token = YOUR_DESEC_API_TOKEN\\ndns_desec_endpoint = https://desec.io/api/v1/\",\n\t\t\"full_plugin_name\": \"dns-desec\"\n\t},\n\t\"duckdns\": {\n\t\t\"name\": \"DuckDNS\",\n\t\t\"package_name\": \"certbot-dns-duckdns\",\n\t\t\"version\": \"~=1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_duckdns_token=your-duckdns-token\",\n\t\t\"full_plugin_name\": \"dns-duckdns\"\n\t},\n\t\"digitalocean\": {\n\t\t\"name\": \"DigitalOcean\",\n\t\t\"package_name\": \"certbot-dns-digitalocean\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff\",\n\t\t\"full_plugin_name\": \"dns-digitalocean\"\n\t},\n\t\"directadmin\": {\n\t\t\"name\": \"DirectAdmin\",\n\t\t\"package_name\": \"certbot-dns-directadmin\",\n\t\t\"version\": \"~=0.0.23\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"directadmin_url = https://my.directadminserver.com:2222\\ndirectadmin_username = username\\ndirectadmin_password = aSuperStrongPassword\",\n\t\t\"full_plugin_name\": \"directadmin\"\n\t},\n\t\"dnsimple\": {\n\t\t\"name\": \"DNSimple\",\n\t\t\"package_name\": \"certbot-dns-dnsimple\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\",\n\t\t\"full_plugin_name\": \"dns-dnsimple\"\n\t},\n\t\"dnsmadeeasy\": {\n\t\t\"name\": \"DNS Made Easy\",\n\t\t\"package_name\": \"certbot-dns-dnsmadeeasy\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\\ndns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55\",\n\t\t\"full_plugin_name\": \"dns-dnsmadeeasy\"\n\t},\n\t\"dnsmulti\": {\n\t\t\"name\": \"DnsMulti\",\n\t\t\"package_name\": \"certbot-dns-multi\",\n\t\t\"version\": \"~=4.9\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# See https://go-acme.github.io/lego/dns/#dns-providers for list of providers and their settings\\n# Example provider configuration for DreamHost\\n# dns_multi_provider = dreamhost\\n# DREAMHOST_API_KEY = ABCDEFG1234\",\n\t\t\"full_plugin_name\": \"dns-multi\"\n\t},\n\t\"dnspod\": {\n\t\t\"name\": \"DNSPod\",\n\t\t\"package_name\": \"certbot-dns-dnspod\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_dnspod_email = \\\"email@example.com\\\"\\ndns_dnspod_api_token = \\\"id,key\\\"\",\n\t\t\"full_plugin_name\": \"dns-dnspod\"\n\t},\n\t\"domainoffensive\": {\n\t\t\"name\": \"DomainOffensive (do.de)\",\n\t\t\"package_name\": \"certbot-dns-domainoffensive\",\n\t\t\"version\": \"~=2.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN\",\n\t\t\"full_plugin_name\": \"dns-domainoffensive\"\n\t},\n\t\"domeneshop\": {\n\t\t\"name\": \"Domeneshop\",\n\t\t\"package_name\": \"certbot-dns-domeneshop\",\n\t\t\"version\": \"~=0.2.8\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_domeneshop_client_token=YOUR_DOMENESHOP_CLIENT_TOKEN\\ndns_domeneshop_client_secret=YOUR_DOMENESHOP_CLIENT_SECRET\",\n\t\t\"full_plugin_name\": \"dns-domeneshop\"\n\t},\n\t\"dynu\": {\n\t\t\"name\": \"Dynu\",\n\t\t\"package_name\": \"certbot-dns-dynu\",\n\t\t\"version\": \"~=0.0.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN\",\n\t\t\"full_plugin_name\": \"dns-dynu\"\n\t},\n\t\"easydns\": {\n\t\t\"name\": \"easyDNS\",\n\t\t\"package_name\": \"certbot-dns-easydns\",\n\t\t\"version\": \"~=0.1.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_easydns_usertoken = YOUR_EASYDNS_USERTOKEN\\ndns_easydns_userkey = YOUR_EASYDNS_USERKEY\\ndns_easydns_endpoint = https://rest.easydns.net\",\n\t\t\"full_plugin_name\": \"dns-easydns\"\n\t},\n\t\"eurodns\": {\n\t\t\"name\": \"EuroDNS\",\n\t\t\"package_name\": \"certbot-dns-eurodns\",\n\t\t\"version\": \"~=0.0.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_eurodns_applicationId = myuser\\ndns_eurodns_apiKey = mysecretpassword\\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy\",\n\t\t\"full_plugin_name\": \"dns-eurodns\"\n\t},\n\t\"firstdomains\": {\n                \"name\": \"First Domains\",\n                \"package_name\": \"certbot-dns-firstdomains\",\n                \"version\": \">=1.0\",\n                \"dependencies\": \"\",\n                \"credentials\": \"dns_firstdomains_username = myremoteuser\\ndns_firstdomains_password = verysecureremoteuserpassword\",\n                \"full_plugin_name\": \"dns-firstdomains\"\n        },\n\t\"freedns\": {\n\t\t\"name\": \"FreeDNS\",\n\t\t\"package_name\": \"certbot-dns-freedns\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_freedns_username = myremoteuser\\ndns_freedns_password = verysecureremoteuserpassword\",\n\t\t\"full_plugin_name\": \"dns-freedns\"\n\t},\n\t\"gandi\": {\n\t\t\"name\": \"Gandi Live DNS\",\n\t\t\"package_name\": \"certbot-dns-gandi\",\n\t\t\"version\": \"~=1.6.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# Gandi personal access token\\ndns_gandi_token=PERSONAL_ACCESS_TOKEN\",\n\t\t\"full_plugin_name\": \"dns-gandi\"\n\t},\n\t\"gcore\": {\n\t\t\"name\": \"Gcore DNS\",\n\t\t\"package_name\": \"certbot-dns-gcore\",\n\t\t\"version\": \"~=0.1.8\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567\",\n\t\t\"full_plugin_name\": \"dns-gcore\"\n\t},\n\t\"glesys\": {\n\t\t\"name\": \"Glesys\",\n\t\t\"package_name\": \"certbot-dns-glesys\",\n\t\t\"version\": \"~=2.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_glesys_user = CL00000\\ndns_glesys_password = apikeyvalue\",\n\t\t\"full_plugin_name\": \"dns-glesys\"\n\t},\n\t\"godaddy\": {\n\t\t\"name\": \"GoDaddy\",\n\t\t\"package_name\": \"certbot-dns-godaddy\",\n\t\t\"version\": \"==2.8.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567\\ndns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123\",\n\t\t\"full_plugin_name\": \"dns-godaddy\"\n\t},\n\t\"google\": {\n\t\t\"name\": \"Google\",\n\t\t\"package_name\": \"certbot-dns-google\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"{\\n\\\"type\\\": \\\"service_account\\\",\\n...\\n}\",\n\t\t\"full_plugin_name\": \"dns-google\"\n\t},\n\t\"googledomains\": {\n\t\t\"name\": \"GoogleDomainsDNS\",\n\t\t\"package_name\": \"certbot-dns-google-domains\",\n\t\t\"version\": \"~=0.1.5\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_google_domains_access_token = 0123456789abcdef0123456789abcdef01234567\\ndns_google_domains_zone = \\\"example.com\\\"\",\n\t\t\"full_plugin_name\": \"dns-google-domains\"\n\t},\n\t\"he\": {\n\t\t\"name\": \"Hurricane Electric\",\n\t\t\"package_name\": \"certbot-dns-he\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_he_user = Me\\ndns_he_pass = my HE password\",\n\t\t\"full_plugin_name\": \"dns-he\"\n\t},\n\t\"he-ddns\": {\n\t\t\"name\": \"Hurricane Electric - DDNS\",\n\t\t\"package_name\": \"certbot-dns-he-ddns\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_he_ddns_password = verysecurepassword\",\n\t\t\"full_plugin_name\": \"dns-he-ddns\"\n\t},\n\t\"hetzner\": {\n\t\t\"name\": \"Hetzner\",\n\t\t\"package_name\": \"certbot-dns-hetzner\",\n\t\t\"version\": \"~=1.0.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_hetzner_api_token = 0123456789abcdef0123456789abcdef\",\n\t\t\"full_plugin_name\": \"dns-hetzner\"\n\t},\n\t\"hetzner-cloud\": {\n\t\t\"name\": \"Hetzner Cloud\",\n\t\t\"package_name\": \"certbot-dns-hetzner-cloud\",\n\t\t\"version\": \"~=1.0.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_hetzner_cloud_api_token = your_api_token_here\",\n\t\t\"full_plugin_name\": \"dns-hetzner-cloud\"\n    },\n\t\"hostingnl\": {\n\t\t\"name\": \"Hosting.nl\",\n\t\t\"package_name\": \"certbot-dns-hostingnl\",\n\t\t\"version\": \"~=0.1.5\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef\",\n\t\t\"full_plugin_name\": \"dns-hostingnl\"\n\t},\n\t\"hover\": {\n\t\t\"name\": \"Hover\",\n\t\t\"package_name\": \"certbot-dns-hover\",\n\t\t\"version\": \"~=1.2.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_hover_hoverurl = https://www.hover.com\\ndns_hover_username = hover-admin-username\\ndns_hover_password = hover-admin-password\\ndns_hover_totpsecret = 2fa-totp-secret\",\n\t\t\"full_plugin_name\": \"dns-hover\"\n\t},\n\t\"infomaniak\": {\n\t\t\"name\": \"Infomaniak\",\n\t\t\"package_name\": \"certbot-dns-infomaniak\",\n\t\t\"version\": \"~=0.2.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n\t\t\"full_plugin_name\": \"dns-infomaniak\"\n\t},\n\t\"inwx\": {\n\t\t\"name\": \"INWX\",\n\t\t\"package_name\": \"certbot-dns-inwx\",\n\t\t\"version\": \"~=2.1.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_inwx_url = https://api.domrobot.com/xmlrpc/\\ndns_inwx_username = your_username\\ndns_inwx_password = your_password\\ndns_inwx_shared_secret = your_shared_secret optional\",\n\t\t\"full_plugin_name\": \"dns-inwx\"\n\t},\n\t\"ionos\": {\n\t\t\"name\": \"IONOS\",\n\t\t\"package_name\": \"certbot-dns-ionos\",\n\t\t\"version\": \"==2022.11.24\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_ionos_prefix = myapikeyprefix\\ndns_ionos_secret = verysecureapikeysecret\\ndns_ionos_endpoint = https://api.hosting.ionos.com\",\n\t\t\"full_plugin_name\": \"dns-ionos\"\n\t},\n\t\"ispconfig\": {\n\t\t\"name\": \"ISPConfig\",\n\t\t\"package_name\": \"certbot-dns-ispconfig\",\n\t\t\"version\": \"~=0.2.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_ispconfig_username = myremoteuser\\ndns_ispconfig_password = verysecureremoteuserpassword\\ndns_ispconfig_endpoint = https://localhost:8080\",\n\t\t\"full_plugin_name\": \"dns-ispconfig\"\n\t},\n\t\"isset\": {\n\t\t\"name\": \"Isset\",\n\t\t\"package_name\": \"certbot-dns-isset\",\n\t\t\"version\": \"~=0.0.3\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_isset_endpoint=\\\"https://customer.isset.net/api\\\"\\ndns_isset_token=\\\"<token>\\\"\",\n\t\t\"full_plugin_name\": \"dns-isset\"\n\t},\n\t\"joker\": {\n\t\t\"name\": \"Joker\",\n\t\t\"package_name\": \"certbot-dns-joker\",\n\t\t\"version\": \"~=1.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_joker_username = <Dynamic DNS Authentication Username>\\ndns_joker_password = <Dynamic DNS Authentication Password>\\ndns_joker_domain = <Dynamic DNS Domain>\",\n\t\t\"full_plugin_name\": \"dns-joker\"\n\t},\n\t\"kas\": {\n        \"name\": \"All-Inkl\",\n        \"package_name\": \"certbot-dns-kas\",\n        \"version\": \"~=0.1.1\",\n        \"dependencies\": \"kasserver\",\n        \"credentials\": \"dns_kas_user = your_kas_user\\ndns_kas_password = your_kas_password\",\n        \"full_plugin_name\": \"dns-kas\"\n    },\n\t\"leaseweb\": {\n\t\t\"name\": \"LeaseWeb\",\n\t\t\"package_name\": \"certbot-dns-leaseweb\",\n\t\t\"version\": \"~=1.0.3\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_leaseweb_api_token = 01234556789\",\n\t\t\"full_plugin_name\": \"dns-leaseweb\"\n\t},\n\t\"linode\": {\n\t\t\"name\": \"Linode\",\n\t\t\"package_name\": \"certbot-dns-linode\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\\ndns_linode_version = [<blank>|3|4]\",\n\t\t\"full_plugin_name\": \"dns-linode\"\n\t},\n\t\"loopia\": {\n\t\t\"name\": \"Loopia\",\n\t\t\"package_name\": \"certbot-dns-loopia\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_loopia_user = user@loopiaapi\\ndns_loopia_password = abcdef0123456789abcdef01234567abcdef0123\",\n\t\t\"full_plugin_name\": \"dns-loopia\"\n\t},\n\t\"luadns\": {\n\t\t\"name\": \"LuaDNS\",\n\t\t\"package_name\": \"certbot-dns-luadns\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_luadns_email = user@example.com\\ndns_luadns_token = 0123456789abcdef0123456789abcdef\",\n\t\t\"full_plugin_name\": \"dns-luadns\"\n\t},\n\t\"mchost24\": {\n\t\t\"name\": \"MC-HOST24\",\n\t\t\"package_name\": \"certbot-dns-mchost24\",\n\t\t\"version\": \"\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\\ndns_mchost24_api_token=<insert obtained API token here>\",\n\t\t\"full_plugin_name\": \"dns-mchost24\"\n\t},\n\t\"mijnhost\": {\n\t\t\"name\": \"mijn.host\",\n\t\t\"package_name\": \"certbot-dns-mijn-host\",\n\t\t\"version\": \"~=0.0.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_mijn_host_api_key=0123456789abcdef0123456789abcdef\",\n\t\t\"full_plugin_name\": \"dns-mijn-host\"\n\t},\n\t\"namecheap\": {\n\t\t\"name\": \"Namecheap\",\n\t\t\"package_name\": \"certbot-dns-namecheap\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_namecheap_username  = 123456\\ndns_namecheap_api_key      = 0123456789abcdef0123456789abcdef01234567\",\n\t\t\"full_plugin_name\": \"dns-namecheap\"\n\t},\n\t\"netcup\": {\n\t\t\"name\": \"netcup\",\n\t\t\"package_name\": \"certbot-dns-netcup\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_netcup_customer_id  = 123456\\ndns_netcup_api_key      = 0123456789abcdef0123456789abcdef01234567\\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123\",\n\t\t\"full_plugin_name\": \"dns-netcup\"\n\t},\n\t\"nicru\": {\n\t\t\"name\": \"nic.ru\",\n\t\t\"package_name\": \"certbot-dns-nicru\",\n\t\t\"version\": \"~=1.0.3\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_nicru_client_id = application-id\\ndns_nicru_client_secret = application-token\\ndns_nicru_username = 0001110/NIC-D\\ndns_nicru_password = password\\ndns_nicru_scope = .+:.+/zones/example.com(/.+)?\\ndns_nicru_service = DNS_SERVICE_NAME\\ndns_nicru_zone = example.com\",\n\t\t\"full_plugin_name\": \"dns-nicru\"\n\t},\n\t\"njalla\": {\n\t\t\"name\": \"Njalla\",\n\t\t\"package_name\": \"certbot-dns-njalla\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_njalla_token = 0123456789abcdef0123456789abcdef01234567\",\n\t\t\"full_plugin_name\": \"dns-njalla\"\n\t},\n\t\"nsone\": {\n\t\t\"name\": \"NS1\",\n\t\t\"package_name\": \"certbot-dns-nsone\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw\",\n\t\t\"full_plugin_name\": \"dns-nsone\"\n\t},\n\t\"oci\": {\n\t\t\"name\": \"Oracle Cloud Infrastructure DNS\",\n\t\t\"package_name\": \"certbot-dns-oci\",\n\t\t\"version\": \"~=0.3.6\",\n\t\t\"dependencies\": \"oci\",\n\t\t\"credentials\": \"[DEFAULT]\\nuser = ocid1.user.oc1...\\nfingerprint = xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx\\ntenancy = ocid1.tenancy.oc1...\\nregion = us-ashburn-1\\nkey_file = ~/.oci/oci_api_key.pem\",\n\t\t\"full_plugin_name\": \"dns-oci\"\n\t},\n\t\"ovh\": {\n\t\t\"name\": \"OVH\",\n\t\t\"package_name\": \"certbot-dns-ovh\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"dns_ovh_endpoint = ovh-eu\\ndns_ovh_application_key = MDAwMDAwMDAwMDAw\\ndns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\\ndns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\",\n\t\t\"full_plugin_name\": \"dns-ovh\"\n\t},\n\t\"plesk\": {\n\t\t\"name\": \"Plesk\",\n\t\t\"package_name\": \"certbot-dns-plesk\",\n\t\t\"version\": \"~=0.3.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_plesk_username = your-username\\ndns_plesk_password = secret\\ndns_plesk_api_url = https://plesk-api-host:8443\",\n\t\t\"full_plugin_name\": \"dns-plesk\"\n\t},\n\t\"porkbun\": {\n\t\t\"name\": \"Porkbun\",\n\t\t\"package_name\": \"certbot-dns-porkbun\",\n\t\t\"version\": \"~=0.11.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_porkbun_key=your-porkbun-api-key\\ndns_porkbun_secret=your-porkbun-api-secret\",\n\t\t\"full_plugin_name\": \"dns-porkbun\"\n\t},\n\t\"powerdns\": {\n\t\t\"name\": \"PowerDNS\",\n\t\t\"package_name\": \"certbot-dns-powerdns\",\n\t\t\"version\": \"~=0.2.1\",\n\t\t\"dependencies\": \"PyYAML==5.3.1\",\n\t\t\"credentials\": \"dns_powerdns_api_url = https://api.mypowerdns.example.org\\ndns_powerdns_api_key = AbCbASsd!@34\",\n\t\t\"full_plugin_name\": \"dns-powerdns\"\n\t},\n\t\"regru\": {\n\t\t\"name\": \"reg.ru\",\n\t\t\"package_name\": \"certbot-regru\",\n\t\t\"version\": \"~=1.0.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_username=username\\ndns_password=password\",\n\t\t\"full_plugin_name\": \"dns\"\n\t},\n\t\"rfc2136\": {\n\t\t\"name\": \"RFC 2136\",\n\t\t\"package_name\": \"certbot-dns-rfc2136\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"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\",\n\t\t\"full_plugin_name\": \"dns-rfc2136\"\n\t},\n\t\"rockenstein\": {\n\t\t\"name\": \"rockenstein AG\",\n\t\t\"package_name\": \"certbot-dns-rockenstein\",\n\t\t\"version\": \"~=1.0.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_rockenstein_token=<token>\",\n\t\t\"full_plugin_name\": \"dns-rockenstein\"\n\t},\n\t\"route53\": {\n\t\t\"name\": \"Route 53 (Amazon)\",\n\t\t\"package_name\": \"certbot-dns-route53\",\n\t\t\"version\": \"=={{certbot-version}}\",\n\t\t\"dependencies\": \"acme=={{certbot-version}}\",\n\t\t\"credentials\": \"[default]\\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\",\n\t\t\"full_plugin_name\": \"dns-route53\"\n\t},\n\t\"simply\": {\n\t\t\"name\": \"Simply\",\n\t\t\"package_name\": \"certbot-dns-simply\",\n\t\t\"version\": \"~=0.1.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_simply_account_name = UExxxxxx\\ndns_simply_api_key = DsHJdsjh2812872sahj\",\n\t\t\"full_plugin_name\": \"dns-simply\"\n\t},\n\t\"spaceship\": {\n\t\t\"name\": \"Spaceship\",\n\t\t\"package_name\": \"certbot-dns-spaceship\",\n\t\t\"version\": \"~=1.0.4\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"[spaceship]\\napi_key=your_api_key\\napi_secret=your_api_secret\",\n\t\t\"full_plugin_name\": \"dns-spaceship\"\n\t},\n\t\"strato\": {\n\t\t\"name\": \"Strato\",\n\t\t\"package_name\": \"certbot-dns-strato\",\n\t\t\"version\": \"~=0.2.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_strato_username = user\\ndns_strato_password = pass\\n# uncomment if youre using two factor authentication:\\n# dns_strato_totp_devicename = 2fa_device\\n# dns_strato_totp_secret = 2fa_secret\\n#\\n# uncomment if domain name contains special characters\\n# insert domain display name as seen on your account page here\\n# dns_strato_domain_display_name = my-punicode-url.de\\n#\\n# if youre not using strato.de or another special endpoint you can customise it below\\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\\n# dns_strato_custom_api_scheme = https\\n# dns_strato_custom_api_host = www.strato.de\\n# dns_strato_custom_api_port = 443\\n# dns_strato_custom_api_path = \\\"/apps/CustomerService\\\"\",\n\t\t\"full_plugin_name\": \"dns-strato\"\n\t},\n\t        \"selectelv2\": {\n                \"name\": \"Selectel api v2\",\n                \"package_name\": \"certbot-dns-selectel-api-v2\",\n                \"version\": \"~=0.3.0\",\n                \"dependencies\": \"\",\n                \"credentials\": \"dns_selectel_api_v2_account_id = your_account_id\\ndns_selectel_api_v2_project_name = your_project\\ndns_selectel_api_v2_username = your_username\\ndns_selectel_api_v2_password = your_password\",\n                \"full_plugin_name\": \"dns-selectel-api-v2\"\n        },\n\t\"timeweb\": {\n\t\t\"name\": \"Timeweb Cloud\",\n\t\t\"package_name\": \"certbot-dns-timeweb\",\n\t\t\"version\": \"~=1.0.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX\",\n\t\t\"full_plugin_name\": \"dns-timeweb\"\n\t},\n\t\"transip\": {\n\t\t\"name\": \"TransIP\",\n\t\t\"package_name\": \"certbot-dns-transip\",\n\t\t\"version\": \"~=0.5.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_transip_username = my_username\\ndns_transip_key_file = /etc/letsencrypt/transip-rsa.key\",\n\t\t\"full_plugin_name\": \"dns-transip\"\n\t},\n\t\"tencentcloud\": {\n\t\t\"name\": \"Tencent Cloud\",\n\t\t\"package_name\": \"certbot-dns-tencentcloud\",\n\t\t\"version\": \"~=2.0.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_tencentcloud_secret_id  = TENCENT_CLOUD_SECRET_ID\\ndns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY\",\n\t\t\"full_plugin_name\": \"dns-tencentcloud\"\n\t},\n\t\"vultr\": {\n\t\t\"name\": \"Vultr\",\n\t\t\"package_name\": \"certbot-dns-vultr\",\n\t\t\"version\": \"~=1.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_vultr_key = YOUR_VULTR_API_KEY\",\n\t\t\"full_plugin_name\": \"dns-vultr\"\n\t},\n\t\"websupport\": {\n\t\t\"name\": \"Websupport.sk\",\n\t\t\"package_name\": \"certbot-dns-websupport\",\n\t\t\"version\": \"~=2.0.1\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_websupport_identifier = <api_key>\\ndns_websupport_secret_key = <secret>\",\n\t\t\"full_plugin_name\": \"dns-websupport\"\n\t},\n\t\"wedos\": {\n\t\t\"name\": \"Wedos\",\n\t\t\"package_name\": \"certbot-dns-wedos\",\n\t\t\"version\": \"~=2.2\",\n\t\t\"dependencies\": \"\",\n\t\t\"credentials\": \"dns_wedos_user = <wedos_registration>\\ndns_wedos_auth = <wapi_password>\",\n\t\t\"full_plugin_name\": \"dns-wedos\"\n\t},\n\t\"edgedns\": {\n\t\t\"name\": \"Akamai Edge DNS\",\n\t\t\"package_name\": \"certbot-plugin-edgedns\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\": \"\",\n\t\t\"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\",\n\t\t\"full_plugin_name\": \"edgedns\"\n\t},\n\t\"zoneedit\": {\n\t\t\"name\": \"ZoneEdit\",\n\t\t\"package_name\": \"certbot-dns-zoneedit\",\n\t\t\"version\": \"~=0.3.2\",\n\t\t\"dependencies\": \"--no-deps dnspython\",\n\t\t\"credentials\": \"dns_zoneedit_user = <login-user-id>\\ndns_zoneedit_token = <dyn-authentication-token>\",\n\t\t\"full_plugin_name\": \"dns-zoneedit\"\n \t}\n}\n"
  },
  {
    "path": "backend/config/README.md",
    "content": "These files are use in development and are not deployed as part of the final product.\n "
  },
  {
    "path": "backend/config/default.json",
    "content": "{\n  \"database\": {\n    \"engine\": \"mysql2\",\n    \"host\": \"db\",\n    \"name\": \"npm\",\n    \"user\": \"npm\",\n    \"password\": \"npm\",\n    \"port\": 3306\n  }\n}\n"
  },
  {
    "path": "backend/config/sqlite-test-db.json",
    "content": "{\n  \"database\": {\n      \"engine\": \"knex-native\",\n      \"knex\": {\n        \"client\": \"better-sqlite3\",\n        \"connection\": {\n          \"filename\": \"/app/config/mydb.sqlite\"\n        },\n        \"pool\": {\n          \"min\": 0,\n          \"max\": 1,\n          \"createTimeoutMillis\": 3000,\n          \"acquireTimeoutMillis\": 30000,\n          \"idleTimeoutMillis\": 30000,\n          \"reapIntervalMillis\": 1000,\n          \"createRetryIntervalMillis\": 100,\n          \"propagateCreateError\": false\n        },\n        \"migrations\": {\n          \"tableName\": \"migrations\",\n          \"stub\": \"src/backend/lib/migrate_template.js\",\n          \"directory\": \"src/backend/migrations\"\n        }\n      }\n    }\n}\n"
  },
  {
    "path": "backend/db.js",
    "content": "import knex from \"knex\";\nimport {configGet, configHas} from \"./lib/config.js\";\n\nlet instance = null;\n\nconst generateDbConfig = () => {\n\tif (!configHas(\"database\")) {\n\t\tthrow new Error(\n\t\t\t\"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/\",\n\t\t);\n\t}\n\n\tconst cfg = configGet(\"database\");\n\n\tif (cfg.engine === \"knex-native\") {\n\t\treturn cfg.knex;\n\t}\n\n\treturn {\n\t\tclient: cfg.engine,\n\t\tconnection: {\n\t\t\thost: cfg.host,\n\t\t\tuser: cfg.user,\n\t\t\tpassword: cfg.password,\n\t\t\tdatabase: cfg.name,\n\t\t\tport:     cfg.port,\n\t\t\t...(cfg.ssl ? { ssl: cfg.ssl } : {})\n\t\t},\n\t\tmigrations: {\n\t\t\ttableName: \"migrations\",\n\t\t},\n\t};\n};\n\nconst getInstance = () => {\n\tif (!instance) {\n\t\tinstance = knex(generateDbConfig());\n\t}\n\treturn instance;\n}\n\nexport default getInstance;\n"
  },
  {
    "path": "backend/index.js",
    "content": "#!/usr/bin/env node\n\nimport app from \"./app.js\";\nimport internalCertificate from \"./internal/certificate.js\";\nimport internalIpRanges from \"./internal/ip_ranges.js\";\nimport { global as logger } from \"./logger.js\";\nimport { migrateUp } from \"./migrate.js\";\nimport { getCompiledSchema } from \"./schema/index.js\";\nimport setup from \"./setup.js\";\n\nconst IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== \"false\";\n\nasync function appStart() {\n\treturn migrateUp()\n\t\t.then(setup)\n\t\t.then(getCompiledSchema)\n\t\t.then(() => {\n\t\t\tif (!IP_RANGES_FETCH_ENABLED) {\n\t\t\t\tlogger.info(\"IP Ranges fetch is disabled by environment variable\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlogger.info(\"IP Ranges fetch is enabled\");\n\t\t\treturn internalIpRanges.fetch().catch((err) => {\n\t\t\t\tlogger.error(\"IP Ranges fetch failed, continuing anyway:\", err.message);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tinternalCertificate.initTimer();\n\t\t\tinternalIpRanges.initTimer();\n\n\t\t\tconst server = app.listen(3000, () => {\n\t\t\t\tlogger.info(`Backend PID ${process.pid} listening on port 3000 ...`);\n\n\t\t\t\tprocess.on(\"SIGTERM\", () => {\n\t\t\t\t\tlogger.info(`PID ${process.pid} received SIGTERM`);\n\t\t\t\t\tserver.close(() => {\n\t\t\t\t\t\tlogger.info(\"Stopping.\");\n\t\t\t\t\t\tprocess.exit(0);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t\t})\n\t\t.catch((err) => {\n\t\t\tlogger.error(`Startup Error: ${err.message}`, err);\n\t\t\tsetTimeout(appStart, 1000);\n\t\t});\n}\n\ntry {\n\tappStart();\n} catch (err) {\n\tlogger.fatal(err);\n\tprocess.exit(1);\n}\n"
  },
  {
    "path": "backend/internal/2fa.js",
    "content": "import crypto from \"node:crypto\";\nimport bcrypt from \"bcrypt\";\nimport { createGuardrails, generateSecret, generateURI, verify } from \"otplib\";\nimport errs from \"../lib/error.js\";\nimport authModel from \"../models/auth.js\";\nimport internalUser from \"./user.js\";\n\nconst APP_NAME = \"Nginx Proxy Manager\";\nconst BACKUP_CODE_COUNT = 8;\n\n/**\n * Generate backup codes\n * @returns {Promise<{plain: string[], hashed: string[]}>}\n */\nconst generateBackupCodes = async () => {\n\tconst plain = [];\n\tconst hashed = [];\n\n\tfor (let i = 0; i < BACKUP_CODE_COUNT; i++) {\n\t\tconst code = crypto.randomBytes(4).toString(\"hex\").toUpperCase();\n\t\tplain.push(code);\n\t\tconst hash = await bcrypt.hash(code, 10);\n\t\thashed.push(hash);\n\t}\n\n\treturn { plain, hashed };\n};\n\nconst internal2fa = {\n\t/**\n\t * Check if user has 2FA enabled\n\t * @param {number} userId\n\t * @returns {Promise<boolean>}\n\t */\n\tisEnabled: async (userId) => {\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\t\treturn auth?.meta?.totp_enabled === true;\n\t},\n\n\t/**\n\t * Get 2FA status for user\n\t * @param   {Access}  access\n\t * @param   {number}  userId\n\t * @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}\n\t */\n\tgetStatus: async (access, userId) => {\n\t\tawait access.can(\"users:password\", userId);\n\t\tawait internalUser.get(access, { id: userId });\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\t\tconst enabled = auth?.meta?.totp_enabled === true;\n\t\tlet backup_codes_remaining = 0;\n\n\t\tif (enabled) {\n\t\t\tconst backupCodes = auth.meta.backup_codes || [];\n\t\t\tbackup_codes_remaining = backupCodes.length;\n\t\t}\n\n\t\treturn {\n\t\t\tenabled,\n\t\t\tbackup_codes_remaining,\n\t\t};\n\t},\n\n\t/**\n\t * Start 2FA setup - store pending secret\n\t *\n\t * @param   {Access}  access\n\t * @param   {number} userId\n\t * @returns {Promise<{secret: string, otpauth_url: string}>}\n\t */\n\tstartSetup: async (access, userId) => {\n\t\tawait access.can(\"users:password\", userId);\n\t\tconst user = await internalUser.get(access, { id: userId });\n\t\tconst secret = generateSecret();\n\t\tconst otpauth_url = generateURI({\n\t\t\tissuer: APP_NAME,\n\t\t\tlabel: user.email,\n\t\t\tsecret: secret,\n\t\t});\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\n\t\t// ensure user isn't already setup for 2fa\n\t\tconst enabled = auth?.meta?.totp_enabled === true;\n\t\tif (enabled) {\n\t\t\tthrow new errs.ValidationError(\"2FA is already enabled\");\n\t\t}\n\n\t\tconst meta = auth.meta || {};\n\t\tmeta.totp_pending_secret = secret;\n\n\t\tawait authModel\n\t\t\t.query()\n\t\t\t.where(\"id\", auth.id)\n\t\t\t.andWhere(\"user_id\", userId)\n\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t.patch({ meta });\n\n\t\treturn { secret, otpauth_url };\n\t},\n\n\t/**\n\t * Enable 2FA after verifying code\n\t *\n\t * @param   {Access}  access\n\t * @param   {number}  userId\n\t * @param   {string}  code\n\t * @returns {Promise<{backup_codes: string[]}>}\n\t */\n\tenable: async (access, userId, code) => {\n\t\tawait access.can(\"users:password\", userId);\n\t\tawait internalUser.get(access, { id: userId });\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\t\tconst secret = auth?.meta?.totp_pending_secret || false;\n\n\t\tif (!secret) {\n\t\t\tthrow new errs.ValidationError(\"No pending 2FA setup found\");\n\t\t}\n\n\t\tconst result = await verify({ token: code, secret });\n\t\tif (!result.valid) {\n\t\t\tthrow new errs.ValidationError(\"Invalid verification code\");\n\t\t}\n\n\t\tconst { plain, hashed } = await generateBackupCodes();\n\n\t\tconst meta = {\n\t\t\t...auth.meta,\n\t\t\ttotp_secret: secret,\n\t\t\ttotp_enabled: true,\n\t\t\ttotp_enabled_at: new Date().toISOString(),\n\t\t\tbackup_codes: hashed,\n\t\t};\n\t\tdelete meta.totp_pending_secret;\n\n\t\tawait authModel\n\t\t\t.query()\n\t\t\t.where(\"id\", auth.id)\n\t\t\t.andWhere(\"user_id\", userId)\n\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t.patch({ meta });\n\n\t\treturn { backup_codes: plain };\n\t},\n\n\t/**\n\t * Disable 2FA\n\t *\n\t * @param   {Access}  access\n\t * @param   {number} userId\n\t * @param   {string} code\n\t * @returns {Promise<void>}\n\t */\n\tdisable: async (access, userId, code) => {\n\t\tawait access.can(\"users:password\", userId);\n\t\tawait internalUser.get(access, { id: userId });\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\n\t\tconst enabled = auth?.meta?.totp_enabled === true;\n\t\tif (!enabled) {\n\t\t\tthrow new errs.ValidationError(\"2FA is not enabled\");\n\t\t}\n\n\t\tconst result = await verify({\n            token: code,\n            secret: auth.meta.totp_secret,\n            guardrails: createGuardrails({\n                MIN_SECRET_BYTES: 10,\n            }),\n        });\n\n\t\tif (!result.valid) {\n\t\t\tthrow new errs.AuthError(\"Invalid verification code\");\n\t\t}\n\n\t\tconst meta = { ...auth.meta };\n\t\tdelete meta.totp_secret;\n\t\tdelete meta.totp_enabled;\n\t\tdelete meta.totp_enabled_at;\n\t\tdelete meta.backup_codes;\n\n\t\tawait authModel\n\t\t\t.query()\n\t\t\t.where(\"id\", auth.id)\n\t\t\t.andWhere(\"user_id\", userId)\n\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t.patch({ meta });\n\t},\n\n\t/**\n\t * Verify 2FA code for login\n\t *\n\t * @param   {number} userId\n\t * @param   {string} token\n\t * @returns {Promise<boolean>}\n\t */\n\tverifyForLogin: async (userId, token) => {\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\t\tconst secret = auth?.meta?.totp_secret || false;\n\n\t\tif (!secret) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars\n\t\t// and the backup codes are 8 chars.\n\t\tif (token.length === 6) {\n\t\t\tconst result = await verify({\n\t\t\t\ttoken,\n\t\t\t\tsecret,\n\t\t\t\t// These guardrails lower the minimum length requirement for secrets.\n\t\t\t\t// In v12 of otplib the default minimum length is 10 and in v13 it is 16.\n\t\t\t\t// Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets\n\t\t\t\t// so people won't be locked out when upgrading.\n\t\t\t\tguardrails: createGuardrails({\n\t\t\t\t\tMIN_SECRET_BYTES: 10,\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (result.valid) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// Try backup codes\n\t\tconst backupCodes = auth?.meta?.backup_codes || [];\n\t\tfor (let i = 0; i < backupCodes.length; i++) {\n\t\t\tconst match = await bcrypt.compare(token.toUpperCase(), backupCodes[i]);\n\t\t\tif (match) {\n\t\t\t\t// Remove used backup code\n\t\t\t\tconst updatedCodes = [...backupCodes];\n\t\t\t\tupdatedCodes.splice(i, 1);\n\t\t\t\tconst meta = { ...auth.meta, backup_codes: updatedCodes };\n\t\t\t\tawait authModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", auth.id)\n\t\t\t\t\t.andWhere(\"user_id\", userId)\n\t\t\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t\t\t.patch({ meta });\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t},\n\n\t/**\n\t * Regenerate backup codes\n\t *\n\t * @param   {Access}  access\n\t * @param   {number}  userId\n\t * @param   {string}  token\n\t * @returns {Promise<{backup_codes: string[]}>}\n\t */\n\tregenerateBackupCodes: async (access, userId, token) => {\n\t\tawait access.can(\"users:password\", userId);\n\t\tawait internalUser.get(access, { id: userId });\n\t\tconst auth = await internal2fa.getUserPasswordAuth(userId);\n\t\tconst enabled = auth?.meta?.totp_enabled === true;\n\t\tconst secret = auth?.meta?.totp_secret || false;\n\n\t\tif (!enabled) {\n\t\t\tthrow new errs.ValidationError(\"2FA is not enabled\");\n\t\t}\n\t\tif (!secret) {\n\t\t\tthrow new errs.ValidationError(\"No 2FA secret found\");\n\t\t}\n\n\t\tconst result = await verify({\n\t\t\ttoken,\n\t\t\tsecret,\n\t\t});\n\n\t\tif (!result.valid) {\n\t\t\tthrow new errs.ValidationError(\"Invalid verification code\");\n\t\t}\n\n\t\tconst { plain, hashed } = await generateBackupCodes();\n\n\t\tconst meta = { ...auth.meta, backup_codes: hashed };\n\t\tawait authModel\n\t\t\t.query()\n\t\t\t.where(\"id\", auth.id)\n\t\t\t.andWhere(\"user_id\", userId)\n\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t.patch({ meta });\n\n\t\treturn { backup_codes: plain };\n\t},\n\n\tgetUserPasswordAuth: async (userId) => {\n\t\tconst auth = await authModel\n\t\t\t.query()\n\t\t\t.where(\"user_id\", userId)\n\t\t\t.andWhere(\"type\", \"password\")\n\t\t\t.first();\n\n\t\tif (!auth) {\n\t\t\tthrow new errs.ItemNotFoundError(\"Auth not found\");\n\t\t}\n\n\t\treturn auth;\n\t},\n};\n\nexport default internal2fa;\n"
  },
  {
    "path": "backend/internal/access-list.js",
    "content": "import fs from \"node:fs\";\nimport batchflow from \"batchflow\";\nimport _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport utils from \"../lib/utils.js\";\nimport { access as logger } from \"../logger.js\";\nimport accessListModel from \"../models/access_list.js\";\nimport accessListAuthModel from \"../models/access_list_auth.js\";\nimport accessListClientModel from \"../models/access_list_client.js\";\nimport proxyHostModel from \"../models/proxy_host.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\"];\n};\n\nconst internalAccessList = {\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: async (access, data) => {\n\t\tawait access.can(\"access_lists:create\", data);\n\t\tconst row = await accessListModel\n\t\t\t.query()\n\t\t\t.insertAndFetch({\n\t\t\t\tname: data.name,\n\t\t\t\tsatisfy_any: data.satisfy_any,\n\t\t\t\tpass_auth: data.pass_auth,\n\t\t\t\towner_user_id: access.token.getUserId(1),\n\t\t\t})\n\t\t\t.then(utils.omitRow(omissions()));\n\n\t\tdata.id = row.id;\n\n\t\tconst promises = [];\n\t\t// Items\n\t\tdata.items.map((item) => {\n\t\t\tpromises.push(\n\t\t\t\taccessListAuthModel.query().insert({\n\t\t\t\t\taccess_list_id: row.id,\n\t\t\t\t\tusername: item.username,\n\t\t\t\t\tpassword: item.password,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn true;\n\t\t});\n\n\t\t// Clients\n\t\tdata.clients?.map((client) => {\n\t\t\tpromises.push(\n\t\t\t\taccessListClientModel.query().insert({\n\t\t\t\t\taccess_list_id: row.id,\n\t\t\t\t\taddress: client.address,\n\t\t\t\t\tdirective: client.directive,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn true;\n\t\t});\n\n\t\tawait Promise.all(promises);\n\n\t\t// re-fetch with expansions\n\t\tconst freshRow = await internalAccessList.get(\n\t\t\taccess,\n\t\t\t{\n\t\t\t\tid: data.id,\n\t\t\t\texpand: [\"owner\", \"items\", \"clients\", \"proxy_hosts.access_list.[clients,items]\"],\n\t\t\t},\n\t\t\ttrue // skip masking\n\t\t);\n\n\t\t// Audit log\n\t\tdata.meta = _.assign({}, data.meta || {}, freshRow.meta);\n\t\tawait internalAccessList.build(freshRow);\n\n\t\tif (Number.parseInt(freshRow.proxy_host_count, 10)) {\n\t\t\tawait internalNginx.bulkGenerateConfigs(\"proxy_host\", freshRow.proxy_hosts);\n\t\t}\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"created\",\n\t\t\tobject_type: \"access-list\",\n\t\t\tobject_id: freshRow.id,\n\t\t\tmeta: internalAccessList.maskItems(data),\n\t\t});\n\n\t\treturn internalAccessList.maskItems(freshRow);\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Integer} data.id\n\t * @param  {String}  [data.name]\n\t * @param  {String}  [data.items]\n\t * @return {Promise}\n\t */\n\tupdate: async (access, data) => {\n\t\tawait access.can(\"access_lists:update\", data.id);\n\t\tconst row = await internalAccessList.get(access, { id: data.id });\n\t\tif (row.id !== data.id) {\n\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`,\n\t\t\t);\n\t\t}\n\n\t\t// patch name if specified\n\t\tif (typeof data.name !== \"undefined\" && data.name) {\n\t\t\tawait accessListModel.query().where({ id: data.id }).patch({\n\t\t\t\tname: data.name,\n\t\t\t\tsatisfy_any: data.satisfy_any,\n\t\t\t\tpass_auth: data.pass_auth,\n\t\t\t});\n\t\t}\n\n\t\t// Check for items and add/update/remove them\n\t\tif (typeof data.items !== \"undefined\" && data.items) {\n\t\t\tconst promises = [];\n\t\t\tconst itemsToKeep = [];\n\n\t\t\tdata.items.map((item) => {\n\t\t\t\tif (item.password) {\n\t\t\t\t\tpromises.push(\n\t\t\t\t\t\taccessListAuthModel.query().insert({\n\t\t\t\t\t\t\taccess_list_id: data.id,\n\t\t\t\t\t\t\tusername: item.username,\n\t\t\t\t\t\t\tpassword: item.password,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\t// This was supplied with an empty password, which means keep it but don't change the password\n\t\t\t\t\titemsToKeep.push(item.username);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tconst query = accessListAuthModel.query().delete().where(\"access_list_id\", data.id);\n\n\t\t\tif (itemsToKeep.length) {\n\t\t\t\tquery.andWhere(\"username\", \"NOT IN\", itemsToKeep);\n\t\t\t}\n\n\t\t\tawait query;\n\t\t\t// Add new items\n\t\t\tif (promises.length) {\n\t\t\t\tawait Promise.all(promises);\n\t\t\t}\n\t\t}\n\n\t\t// Check for clients and add/update/remove them\n\t\tif (typeof data.clients !== \"undefined\" && data.clients) {\n\t\t\tconst clientPromises = [];\n\t\t\tdata.clients.map((client) => {\n\t\t\t\tif (client.address) {\n\t\t\t\t\tclientPromises.push(\n\t\t\t\t\t\taccessListClientModel.query().insert({\n\t\t\t\t\t\t\taccess_list_id: data.id,\n\t\t\t\t\t\t\taddress: client.address,\n\t\t\t\t\t\t\tdirective: client.directive,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tconst query = accessListClientModel.query().delete().where(\"access_list_id\", data.id);\n\t\t\tawait query;\n\t\t\t// Add new clitens\n\t\t\tif (clientPromises.length) {\n\t\t\t\tawait Promise.all(clientPromises);\n\t\t\t}\n\t\t}\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"updated\",\n\t\t\tobject_type: \"access-list\",\n\t\t\tobject_id: data.id,\n\t\t\tmeta: internalAccessList.maskItems(data),\n\t\t});\n\n\t\t// re-fetch with expansions\n\t\tconst freshRow = await internalAccessList.get(\n\t\t\taccess,\n\t\t\t{\n\t\t\t\tid: data.id,\n\t\t\t\texpand: [\"owner\", \"items\", \"clients\", \"proxy_hosts.[certificate,access_list.[clients,items]]\"],\n\t\t\t},\n\t\t\ttrue // skip masking\n\t\t);\n\n\t\tawait internalAccessList.build(freshRow)\n\t\tif (Number.parseInt(freshRow.proxy_host_count, 10)) {\n\t\t\tawait internalNginx.bulkGenerateConfigs(\"proxy_host\", freshRow.proxy_hosts);\n\t\t}\n\t\tawait internalNginx.reload();\n\t\treturn internalAccessList.maskItems(freshRow);\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Integer}  data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @param  {Boolean}  [skipMasking]\n\t * @return {Promise}\n\t */\n\tget: async (access, data, skipMasking) => {\n\t\tconst thisData = data || {};\n\t\tconst accessData = await access.can(\"access_lists:get\", thisData.id)\n\n\t\tconst query = accessListModel\n\t\t\t.query()\n\t\t\t.select(\"access_list.*\", accessListModel.raw(\"COUNT(proxy_host.id) as proxy_host_count\"))\n\t\t\t.leftJoin(\"proxy_host\", function () {\n\t\t\t\tthis.on(\"proxy_host.access_list_id\", \"=\", \"access_list.id\").andOn(\n\t\t\t\t\t\"proxy_host.is_deleted\",\n\t\t\t\t\t\"=\",\n\t\t\t\t\t0,\n\t\t\t\t);\n\t\t\t})\n\t\t\t.where(\"access_list.is_deleted\", 0)\n\t\t\t.andWhere(\"access_list.id\", thisData.id)\n\t\t\t.groupBy(\"access_list.id\")\n\t\t\t.allowGraph(\"[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]\")\n\t\t\t.first();\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"access_list.owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\tif (typeof thisData.expand !== \"undefined\" && thisData.expand !== null) {\n\t\t\tquery.withGraphFetched(`[${thisData.expand.join(\", \")}]`);\n\t\t}\n\n\t\tlet row = await query.then(utils.omitRow(omissions()));\n\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(thisData.id);\n\t\t}\n\t\tif (!skipMasking && typeof row.items !== \"undefined\" && row.items) {\n\t\t\trow = internalAccessList.maskItems(row);\n\t\t}\n\t\t// Custom omissions\n\t\tif (typeof data.omit !== \"undefined\" && data.omit !== null) {\n\t\t\trow = _.omit(row, data.omit);\n\t\t}\n\t\treturn row;\n\t},\n\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @param   {Integer} data.id\n\t * @param   {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: async (access, data) => {\n\t\tawait access.can(\"access_lists:delete\", data.id);\n\t\tconst row = await internalAccessList.get(access, {\n\t\t\tid: data.id,\n\t\t\texpand: [\"proxy_hosts\", \"items\", \"clients\"],\n\t\t});\n\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\n\t\t// 1. update row to be deleted\n\t\t// 2. update any proxy hosts that were using it (ignoring permissions)\n\t\t// 3. reconfigure those hosts\n\t\t// 4. audit log\n\n\t\t// 1. update row to be deleted\n\t\tawait accessListModel\n\t\t\t.query()\n\t\t\t.where(\"id\", row.id)\n\t\t\t.patch({\n\t\t\t\tis_deleted: 1,\n\t\t\t});\n\n\t\t// 2. update any proxy hosts that were using it (ignoring permissions)\n\t\tif (row.proxy_hosts) {\n\t\t\tawait proxyHostModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"access_list_id\", \"=\", row.id)\n\t\t\t\t.patch({ access_list_id: 0 });\n\n\t\t\t// 3. reconfigure those hosts, then reload nginx\n\t\t\t// set the access_list_id to zero for these items\n\t\t\trow.proxy_hosts.map((_val, idx) => {\n\t\t\t\trow.proxy_hosts[idx].access_list_id = 0;\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tawait internalNginx.bulkGenerateConfigs(\"proxy_host\", row.proxy_hosts);\n\t\t}\n\n\t\tawait internalNginx.reload();\n\n\t\t// delete the htpasswd file\n\t\ttry {\n\t\t\tfs.unlinkSync(internalAccessList.getFilename(row));\n\t\t} catch (_err) {\n\t\t\t// do nothing\n\t\t}\n\n\t\t// 4. audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"deleted\",\n\t\t\tobject_type: \"access-list\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(internalAccessList.maskItems(row), [\"is_deleted\", \"proxy_hosts\"]),\n\t\t});\n\t\treturn true;\n\t},\n\n\t/**\n\t * All Lists\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [searchQuery]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, searchQuery) => {\n\t\tconst accessData = await access.can(\"access_lists:list\");\n\n\t\tconst query = accessListModel\n\t\t\t.query()\n\t\t\t.select(\"access_list.*\", accessListModel.raw(\"COUNT(proxy_host.id) as proxy_host_count\"))\n\t\t\t.leftJoin(\"proxy_host\", function () {\n\t\t\t\tthis.on(\"proxy_host.access_list_id\", \"=\", \"access_list.id\").andOn(\n\t\t\t\t\t\"proxy_host.is_deleted\",\n\t\t\t\t\t\"=\",\n\t\t\t\t\t0,\n\t\t\t\t);\n\t\t\t})\n\t\t\t.where(\"access_list.is_deleted\", 0)\n\t\t\t.groupBy(\"access_list.id\")\n\t\t\t.allowGraph(\"[owner,items,clients]\")\n\t\t\t.orderBy(\"access_list.name\", \"ASC\");\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"access_list.owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\t// Query is used for searching\n\t\tif (typeof searchQuery === \"string\") {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(\"name\", \"like\", `%${searchQuery}%`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst rows = await query.then(utils.omitRows(omissions()));\n\t\tif (rows) {\n\t\t\trows.map((row, idx) => {\n\t\t\t\tif (typeof row.items !== \"undefined\" && row.items) {\n\t\t\t\t\trows[idx] = internalAccessList.maskItems(row);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\treturn rows;\n\t},\n\n\t/**\n\t * Count is used in reports\n\t *\n\t * @param   {Integer} userId\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: async (userId, visibility) => {\n\t\tconst query = accessListModel\n\t\t\t.query()\n\t\t\t.count(\"id as count\")\n\t\t\t.where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", userId);\n\t\t}\n\n\t\tconst row = await query.first();\n\t\treturn Number.parseInt(row.count, 10);\n\t},\n\n\t/**\n\t * @param   {Object}  list\n\t * @returns {Object}\n\t */\n\tmaskItems: (list) => {\n\t\tif (list && typeof list.items !== \"undefined\") {\n\t\t\tlist.items.map((val, idx) => {\n\t\t\t\tlet repeatFor = 8;\n\t\t\t\tlet firstChar = \"*\";\n\n\t\t\t\tif (typeof val.password !== \"undefined\" && val.password) {\n\t\t\t\t\trepeatFor = val.password.length - 1;\n\t\t\t\t\tfirstChar = val.password.charAt(0);\n\t\t\t\t}\n\n\t\t\t\tlist.items[idx].hint = firstChar + \"*\".repeat(repeatFor);\n\t\t\t\tlist.items[idx].password = \"\";\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\treturn list;\n\t},\n\n\t/**\n\t * @param   {Object}  list\n\t * @param   {Integer} list.id\n\t * @returns {String}\n\t */\n\tgetFilename: (list) => {\n\t\treturn `/data/access/${list.id}`;\n\t},\n\n\t/**\n\t * @param   {Object}  list\n\t * @param   {Integer} list.id\n\t * @param   {String}  list.name\n\t * @param   {Array}   list.items\n\t * @returns {Promise}\n\t */\n\tbuild: async (list) => {\n\t\tlogger.info(`Building Access file #${list.id} for: ${list.name}`);\n\n\t\tconst htpasswdFile = internalAccessList.getFilename(list);\n\n\t\t// 1. remove any existing access file\n\t\ttry {\n\t\t\tfs.unlinkSync(htpasswdFile);\n\t\t} catch (_err) {\n\t\t\t// do nothing\n\t\t}\n\n\t\t// 2. create empty access file\n\t\tfs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});\n\n\t\t// 3. generate password for each user\n\t\tif (list.items.length) {\n\t\t\tawait new Promise((resolve, reject) => {\n\t\t\t\tbatchflow(list.items).sequential()\n\t\t\t\t\t.each((_i, item, next) => {\n\t\t\t\t\t\tif (item.password?.length) {\n\t\t\t\t\t\t\tlogger.info(`Adding: ${item.username}`);\n\n\t\t\t\t\t\t\tutils.execFile('openssl', ['passwd', '-apr1', item.password])\n\t\t\t\t\t\t\t\t.then((res) => {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tfs.appendFileSync(htpasswdFile, `${item.username}:${res}\\n`, {encoding: 'utf8'});\n\t\t\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\t\t\treject(err);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tnext();\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\t\t\tlogger.error(err);\n\t\t\t\t\t\t\t\t\tnext(err);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.error((err) => {\n\t\t\t\t\t\tlogger.error(err);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t})\n\t\t\t\t\t.end((results) => {\n\t\t\t\t\t\tlogger.success(`Built Access file #${list.id} for: ${list.name}`);\n\t\t\t\t\t\tresolve(results);\n\t\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n}\n\nexport default internalAccessList;\n"
  },
  {
    "path": "backend/internal/audit-log.js",
    "content": "import errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport auditLogModel from \"../models/audit-log.js\";\n\nconst internalAuditLog = {\n\n\t/**\n\t * All logs\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [searchQuery]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, searchQuery) => {\n\t\tawait access.can(\"auditlog:list\");\n\n\t\tconst query = auditLogModel\n\t\t\t.query()\n\t\t\t.orderBy(\"created_on\", \"DESC\")\n\t\t\t.orderBy(\"id\", \"DESC\")\n\t\t\t.limit(100)\n\t\t\t.allowGraph(\"[user]\");\n\n\t\t// Query is used for searching\n\t\tif (typeof searchQuery === \"string\" && searchQuery.length > 0) {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(castJsonIfNeed(\"meta\"), \"like\", `%${searchQuery}`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\treturn await query;\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   [data]\n\t * @param  {Integer}  [data.id]          Defaults to the token user\n\t * @param  {Array}    [data.expand]\n\t * @return {Promise}\n\t */\n\tget: async (access, data) => {\n\t\tawait access.can(\"auditlog:list\");\n\n\t\tconst query = auditLogModel\n\t\t\t.query()\n\t\t\t.andWhere(\"id\", data.id)\n\t\t\t.allowGraph(\"[user]\")\n\t\t\t.first();\n\n\t\tif (typeof data.expand !== \"undefined\" && data.expand !== null) {\n\t\t\tquery.withGraphFetched(`[${data.expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst row = await query;\n\n\t\tif (!row?.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\n\t\treturn row;\n\t},\n\n\t/**\n\t * This method should not be publicly used, it doesn't check certain things. It will be assumed\n\t * that permission to add to audit log is already considered, however the access token is used for\n\t * default user id determination.\n\t *\n\t * @param   {Access}   access\n\t * @param   {Object}   data\n\t * @param   {String}   data.action\n\t * @param   {Number}   [data.user_id]\n\t * @param   {Number}   [data.object_id]\n\t * @param   {Number}   [data.object_type]\n\t * @param   {Object}   [data.meta]\n\t * @returns {Promise}\n\t */\n\tadd: async (access, data) => {\n\t\tif (typeof data.user_id === \"undefined\" || !data.user_id) {\n\t\t\tdata.user_id = access.token.getUserId(1);\n\t\t}\n\n\t\tif (typeof data.action === \"undefined\" || !data.action) {\n\t\t\tthrow new errs.InternalValidationError(\"Audit log entry must contain an Action\");\n\t\t}\n\n\t\t// Make sure at least 1 of the IDs are set and action\n\t\treturn await auditLogModel.query().insert({\n\t\t\tuser_id: data.user_id,\n\t\t\taction: data.action,\n\t\t\tobject_type: data.object_type || \"\",\n\t\t\tobject_id: data.object_id || 0,\n\t\t\tmeta: data.meta || {},\n\t\t});\n\t},\n};\n\nexport default internalAuditLog;\n"
  },
  {
    "path": "backend/internal/certificate.js",
    "content": "import fs from \"node:fs\";\nimport https from \"node:https\";\nimport path from \"path\";\nimport archiver from \"archiver\";\nimport _ from \"lodash\";\nimport moment from \"moment\";\nimport { ProxyAgent } from \"proxy-agent\";\nimport tempWrite from \"temp-write\";\nimport dnsPlugins from \"../certbot/dns-plugins.json\" with { type: \"json\" };\nimport { installPlugin } from \"../lib/certbot.js\";\nimport { useLetsencryptServer, useLetsencryptStaging } from \"../lib/config.js\";\nimport error from \"../lib/error.js\";\nimport utils from \"../lib/utils.js\";\nimport { debug, ssl as logger } from \"../logger.js\";\nimport certificateModel from \"../models/certificate.js\";\nimport tokenModel from \"../models/token.js\";\nimport userModel from \"../models/user.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalHost from \"./host.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst letsencryptConfig = \"/etc/letsencrypt.ini\";\nconst certbotCommand = \"certbot\";\nconst certbotLogsDir = \"/data/logs\";\nconst certbotWorkDir = \"/tmp/letsencrypt-lib\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\", \"owner.is_deleted\", \"meta.dns_provider_credentials\"];\n};\n\nconst internalCertificate = {\n\tallowedSslFiles: [\"certificate\", \"certificate_key\", \"intermediate_certificate\"],\n\tintervalTimeout: 1000 * 60 * 60, // 1 hour\n\tinterval: null,\n\tintervalProcessing: false,\n\trenewBeforeExpirationBy: [30, \"days\"],\n\n\tinitTimer: () => {\n\t\tlogger.info(\"Let's Encrypt Renewal Timer initialized\");\n\t\tinternalCertificate.interval = setInterval(\n\t\t\tinternalCertificate.processExpiringHosts,\n\t\t\tinternalCertificate.intervalTimeout,\n\t\t);\n\t\t// And do this now as well\n\t\tinternalCertificate.processExpiringHosts();\n\t},\n\n\t/**\n\t * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required\n\t */\n\tprocessExpiringHosts: () => {\n\t\tif (!internalCertificate.intervalProcessing) {\n\t\t\tinternalCertificate.intervalProcessing = true;\n\t\t\tlogger.info(\n\t\t\t\t`Renewing SSL certs expiring within ${internalCertificate.renewBeforeExpirationBy[0]} ${internalCertificate.renewBeforeExpirationBy[1]} ...`,\n\t\t\t);\n\n\t\t\tconst expirationThreshold = moment()\n\t\t\t\t.add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1])\n\t\t\t\t.format(\"YYYY-MM-DD HH:mm:ss\");\n\n\t\t\t// Fetch all the letsencrypt certs from the db that will expire within the configured threshold\n\t\t\tcertificateModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t.andWhere(\"provider\", \"letsencrypt\")\n\t\t\t\t.andWhere(\"expires_on\", \"<\", expirationThreshold)\n\t\t\t\t.then((certificates) => {\n\t\t\t\t\tif (!certificates || !certificates.length) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Renews must be run sequentially or we'll get an error 'Another\n\t\t\t\t\t * instance of Certbot is already running.'\n\t\t\t\t\t */\n\t\t\t\t\tlet sequence = Promise.resolve();\n\n\t\t\t\t\tcertificates.forEach((certificate) => {\n\t\t\t\t\t\tsequence = sequence.then(() =>\n\t\t\t\t\t\t\tinternalCertificate\n\t\t\t\t\t\t\t\t.renew(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tcan: () =>\n\t\t\t\t\t\t\t\t\t\t\tPromise.resolve({\n\t\t\t\t\t\t\t\t\t\t\t\tpermission_visibility: \"all\",\n\t\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t\ttoken: tokenModel(),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ id: certificate.id },\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\t\t\t// Don't want to stop the train here, just log the error\n\t\t\t\t\t\t\t\t\tlogger.error(err.message);\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\n\t\t\t\t\treturn sequence;\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\tlogger.info(\"Completed SSL cert renew process\");\n\t\t\t\t\tinternalCertificate.intervalProcessing = false;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlogger.error(err);\n\t\t\t\t\tinternalCertificate.intervalProcessing = false;\n\t\t\t\t});\n\t\t}\n\t},\n\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: async (access, data) => {\n\t\tawait access.can(\"certificates:create\", data);\n\t\tdata.owner_user_id = access.token.getUserId(1);\n\n\t\tif (data.provider === \"letsencrypt\") {\n\t\t\tdata.nice_name = data.domain_names.join(\", \");\n\t\t}\n\n\t\t// this command really should clean up and delete the cert if it can't fully succeed\n\t\tconst certificate = await certificateModel.query().insertAndFetch(data);\n\n\t\ttry {\n\t\t\tif (certificate.provider === \"letsencrypt\") {\n\t\t\t\t// Request a new Cert from LE. Let the fun begin.\n\n\t\t\t\t// 1. Find out any hosts that are using any of the hostnames in this cert\n\t\t\t\t// 2. Disable them in nginx temporarily\n\t\t\t\t// 3. Generate the LE config\n\t\t\t\t// 4. Request cert\n\t\t\t\t// 5. Remove LE config\n\t\t\t\t// 6. Re-instate previously disabled hosts\n\n\t\t\t\t// 1. Find out any hosts that are using any of the hostnames in this cert\n\t\t\t\tconst inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names);\n\n\t\t\t\t// 2. Disable them in nginx temporarily\n\t\t\t\tawait internalCertificate.disableInUseHosts(inUseResult);\n\n\t\t\t\tconst user = await userModel.query().where(\"is_deleted\", 0).andWhere(\"id\", data.owner_user_id).first();\n\t\t\t\tif (!user || !user.email) {\n\t\t\t\t\tthrow new error.ValidationError(\n\t\t\t\t\t\t\"A valid email address must be set on your user account to use Let's Encrypt\",\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// With DNS challenge no config is needed, so skip 3 and 5.\n\t\t\t\tif (certificate.meta?.dns_challenge) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\t// 4. Request cert\n\t\t\t\t\t\tawait internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email);\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\t// 6. Re-instate previously disabled hosts\n\t\t\t\t\t\tawait internalCertificate.enableInUseHosts(inUseResult);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t// In the event of failure, revert things and throw err back\n\t\t\t\t\t\tawait internalCertificate.enableInUseHosts(inUseResult);\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// 3. Generate the LE config\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait internalNginx.generateLetsEncryptRequestConfig(certificate);\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\tsetTimeout(() => {}, 5000);\n\t\t\t\t\t\t// 4. Request cert\n\t\t\t\t\t\tawait internalCertificate.requestLetsEncryptSsl(certificate, user.email);\n\t\t\t\t\t\t// 5. Remove LE config\n\t\t\t\t\t\tawait internalNginx.deleteLetsEncryptRequestConfig(certificate);\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\t// 6. Re-instate previously disabled hosts\n\t\t\t\t\t\tawait internalCertificate.enableInUseHosts(inUseResult);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t// In the event of failure, revert things and throw err back\n\t\t\t\t\t\tawait internalNginx.deleteLetsEncryptRequestConfig(certificate);\n\t\t\t\t\t\tawait internalCertificate.enableInUseHosts(inUseResult);\n\t\t\t\t\t\tawait internalNginx.reload();\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// At this point, the letsencrypt cert should exist on disk.\n\t\t\t\t// Lets get the expiry date from the file and update the row silently\n\t\t\t\ttry {\n\t\t\t\t\tconst certInfo = await internalCertificate.getCertificateInfoFromFile(\n\t\t\t\t\t\t`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,\n\t\t\t\t\t);\n\t\t\t\t\tconst savedRow = await certificateModel\n\t\t\t\t\t\t.query()\n\t\t\t\t\t\t.patchAndFetchById(certificate.id, {\n\t\t\t\t\t\t\texpires_on: moment(certInfo.dates.to, \"X\").format(\"YYYY-MM-DD HH:mm:ss\"),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(utils.omitRow(omissions()));\n\n\t\t\t\t\t// Add cert data for audit log\n\t\t\t\t\tsavedRow.meta = _.assign({}, savedRow.meta, {\n\t\t\t\t\t\tletsencrypt_certificate: certInfo,\n\t\t\t\t\t});\n\n\t\t\t\t\tawait internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow);\n\n\t\t\t\t\treturn savedRow;\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Delete the certificate from the database if it was not created successfully\n\t\t\t\t\tawait certificateModel.query().deleteById(certificate.id);\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\t// Delete the certificate here. This is a hard delete, since it never existed properly\n\t\t\tawait certificateModel.query().deleteById(certificate.id);\n\t\t\tthrow err;\n\t\t}\n\n\t\tdata.meta = _.assign({}, data.meta || {}, certificate.meta);\n\n\t\t// Add to audit log\n\t\tawait internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data));\n\n\t\treturn utils.omitRow(omissions())(certificate);\n\t},\n\n\taddCreatedAuditLog: async (access, certificate_id, meta) => {\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"created\",\n\t\t\tobject_type: \"certificate\",\n\t\t\tobject_id: certificate_id,\n\t\t\tmeta: meta,\n\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Number}  data.id\n\t * @param  {String}  [data.email]\n\t * @param  {String}  [data.name]\n\t * @return {Promise}\n\t */\n\tupdate: async (access, data) => {\n\t\tawait access.can(\"certificates:update\", data.id);\n\t\tconst row = await internalCertificate.get(access, { id: data.id });\n\n\t\tif (row.id !== data.id) {\n\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\tthrow new error.InternalValidationError(\n\t\t\t\t`Certificate could not be updated, IDs do not match: ${row.id} !== ${data.id}`,\n\t\t\t);\n\t\t}\n\n\t\tconst savedRow = await certificateModel\n\t\t\t.query()\n\t\t\t.patchAndFetchById(row.id, data)\n\t\t\t.then(utils.omitRow(omissions()));\n\n\t\tsavedRow.meta = internalCertificate.cleanMeta(savedRow.meta);\n\t\tdata.meta = internalCertificate.cleanMeta(data.meta);\n\n\t\t// Add row.nice_name for custom certs\n\t\tif (savedRow.provider === \"other\") {\n\t\t\tdata.nice_name = savedRow.nice_name;\n\t\t}\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"updated\",\n\t\t\tobject_type: \"certificate\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(data, [\"expires_on\"]), // this prevents json circular reference because expires_on might be raw\n\t\t});\n\n\t\treturn savedRow;\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Number}   data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: async (access, data) => {\n\t\tconst accessData = await access.can(\"certificates:get\", data.id);\n\t\tconst query = certificateModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.andWhere(\"id\", data.id)\n\t\t\t.allowGraph(\"[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]\")\n\t\t\t.first();\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\tif (typeof data.expand !== \"undefined\" && data.expand !== null) {\n\t\t\tquery.withGraphFetched(`[${data.expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst row = await query.then(utils.omitRow(omissions()));\n\t\tif (!row || !row.id) {\n\t\t\tthrow new error.ItemNotFoundError(data.id);\n\t\t}\n\t\t// Custom omissions\n\t\tif (typeof data.omit !== \"undefined\" && data.omit !== null) {\n\t\t\treturn _.omit(row, [...data.omit]);\n\t\t}\n\n\t\treturn internalCertificate.cleanExpansions(row);\n\t},\n\n\tcleanExpansions: (row) => {\n\t\tif (typeof row.proxy_hosts !== \"undefined\") {\n\t\t\trow.proxy_hosts = utils.omitRows([\"is_deleted\"])(row.proxy_hosts);\n\t\t}\n\t\tif (typeof row.redirection_hosts !== \"undefined\") {\n\t\t\trow.redirection_hosts = utils.omitRows([\"is_deleted\"])(row.redirection_hosts);\n\t\t}\n\t\tif (typeof row.dead_hosts !== \"undefined\") {\n\t\t\trow.dead_hosts = utils.omitRows([\"is_deleted\"])(row.dead_hosts);\n\t\t}\n\t\tif (typeof row.streams !== \"undefined\") {\n\t\t\trow.streams = utils.omitRows([\"is_deleted\"])(row.streams);\n\t\t}\n\t\treturn row;\n\t},\n\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @param   {Number}  data.id\n\t * @returns {Promise}\n\t */\n\tdownload: async (access, data) => {\n\t\tawait access.can(\"certificates:get\", data);\n\t\tconst certificate = await internalCertificate.get(access, data);\n\t\tif (certificate.provider === \"letsencrypt\") {\n\t\t\tconst zipDirectory = internalCertificate.getLiveCertPath(data.id);\n\t\t\tif (!fs.existsSync(zipDirectory)) {\n\t\t\t\tthrow new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`);\n\t\t\t}\n\n\t\t\tconst certFiles = fs\n\t\t\t\t.readdirSync(zipDirectory)\n\t\t\t\t.filter((fn) => fn.endsWith(\".pem\"))\n\t\t\t\t.map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));\n\n\t\t\tconst downloadName = `npm-${data.id}-${Date.now()}.zip`;\n\t\t\tconst opName = `/tmp/${downloadName}`;\n\n\t\t\tawait internalCertificate.zipFiles(certFiles, opName);\n\t\t\tdebug(logger, \"zip completed : \", opName);\n\t\t\treturn {\n\t\t\t\tfileName: opName,\n\t\t\t};\n\t\t}\n\t\tthrow new error.ValidationError(\"Only Let'sEncrypt certificates can be downloaded\");\n\t},\n\n\t/**\n\t * @param   {String}  source\n\t * @param   {String}  out\n\t * @returns {Promise}\n\t */\n\tzipFiles: async (source, out) => {\n\t\tconst archive = archiver(\"zip\", { zlib: { level: 9 } });\n\t\tconst stream = fs.createWriteStream(out);\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tsource.map((fl) => {\n\t\t\t\tconst fileName = path.basename(fl);\n\t\t\t\tdebug(logger, fl, \"added to certificate zip\");\n\t\t\t\tarchive.file(fl, { name: fileName });\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tarchive.on(\"error\", (err) => reject(err)).pipe(stream);\n\t\t\tstream.on(\"close\", () => resolve());\n\t\t\tarchive.finalize();\n\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: async (access, data) => {\n\t\tawait access.can(\"certificates:delete\", data.id);\n\t\tconst row = await internalCertificate.get(access, { id: data.id });\n\n\t\tif (!row || !row.id) {\n\t\t\tthrow new error.ItemNotFoundError(data.id);\n\t\t}\n\n\t\tawait certificateModel.query().where(\"id\", row.id).patch({\n\t\t\tis_deleted: 1,\n\t\t});\n\n\t\t// Add to audit log\n\t\trow.meta = internalCertificate.cleanMeta(row.meta);\n\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"deleted\",\n\t\t\tobject_type: \"certificate\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(row, omissions()),\n\t\t});\n\n\t\tif (row.provider === \"letsencrypt\") {\n\t\t\t// Revoke the cert\n\t\t\tawait internalCertificate.revokeLetsEncryptSsl(row);\n\t\t}\n\t\treturn true;\n\t},\n\n\t/**\n\t * All Certs\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [searchQuery]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, searchQuery) => {\n\t\tconst accessData = await access.can(\"certificates:list\");\n\n\t\tconst query = certificateModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.groupBy(\"id\")\n\t\t\t.allowGraph(\"[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]\")\n\t\t\t.orderBy(\"nice_name\", \"ASC\");\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\t// Query is used for searching\n\t\tif (typeof searchQuery === \"string\") {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(\"nice_name\", \"like\", `%${searchQuery}%`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst r = await query.then(utils.omitRows(omissions()));\n\t\tfor (let i = 0; i < r.length; i++) {\n\t\t\tr[i] = internalCertificate.cleanExpansions(r[i]);\n\t\t}\n\t\treturn r;\n\t},\n\n\t/**\n\t * Report use\n\t *\n\t * @param   {Number}  userId\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: async (userId, visibility) => {\n\t\tconst query = certificateModel.query().count(\"id as count\").where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", userId);\n\t\t}\n\n\t\tconst row = await query.first();\n\t\treturn Number.parseInt(row.count, 10);\n\t},\n\n\t/**\n\t * @param   {Object} certificate\n\t * @returns {Promise}\n\t */\n\twriteCustomCert: async (certificate) => {\n\t\tlogger.info(\"Writing Custom Certificate:\", certificate);\n\n\t\tconst dir = `/data/custom_ssl/npm-${certificate.id}`;\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (certificate.provider === \"letsencrypt\") {\n\t\t\t\treject(new Error(\"Refusing to write letsencrypt certs here\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet certData = certificate.meta.certificate;\n\t\t\tif (typeof certificate.meta.intermediate_certificate !== \"undefined\") {\n\t\t\t\tcertData = `${certData}\\n${certificate.meta.intermediate_certificate}`;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fs.existsSync(dir)) {\n\t\t\t\t\tfs.mkdirSync(dir);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\treject(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfs.writeFile(`${dir}/fullchain.pem`, certData, (err) => {\n\t\t\t\tif (err) {\n\t\t\t\t\treject(err);\n\t\t\t\t} else {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t}).then(() => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tfs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => {\n\t\t\t\t\tif (err) {\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t},\n\n\t/**\n\t * @param   {Access}   access\n\t * @param   {Object}   data\n\t * @param   {Array}    data.domain_names\n\t * @returns {Promise}\n\t */\n\tcreateQuickCertificate: async (access, data) => {\n\t\treturn await internalCertificate.create(access, {\n\t\t\tprovider: \"letsencrypt\",\n\t\t\tdomain_names: data.domain_names,\n\t\t\tmeta: data.meta,\n\t\t});\n\t},\n\n\t/**\n\t * Validates that the certs provided are good.\n\t * No access required here, nothing is changed or stored.\n\t *\n\t * @param   {Object}  data\n\t * @param   {Object}  data.files\n\t * @returns {Promise}\n\t */\n\tvalidate: (data) => {\n\t\t// Put file contents into an object\n\t\tconst files = {};\n\t\t_.map(data.files, (file, name) => {\n\t\t\tif (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {\n\t\t\t\tfiles[name] = file.data.toString();\n\t\t\t}\n\t\t});\n\n\t\t// For each file, create a temp file and write the contents to it\n\t\t// Then test it depending on the file type\n\t\tconst promises = [];\n\t\t_.map(files, (content, type) => {\n\t\t\tpromises.push(\n\t\t\t\tnew Promise((resolve) => {\n\t\t\t\t\tif (type === \"certificate_key\") {\n\t\t\t\t\t\tresolve(internalCertificate.checkPrivateKey(content));\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// this should handle `certificate` and intermediate certificate\n\t\t\t\t\t\tresolve(internalCertificate.getCertificateInfo(content, true));\n\t\t\t\t\t}\n\t\t\t\t}).then((res) => {\n\t\t\t\t\treturn { [type]: res };\n\t\t\t\t}),\n\t\t\t);\n\t\t});\n\n\t\treturn Promise.all(promises).then((files) => {\n\t\t\tlet data = {};\n\t\t\t_.each(files, (file) => {\n\t\t\t\tdata = _.assign({}, data, file);\n\t\t\t});\n\t\t\treturn data;\n\t\t});\n\t},\n\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @param   {Number}  data.id\n\t * @param   {Object}  data.files\n\t * @returns {Promise}\n\t */\n\tupload: async (access, data) => {\n\t\tconst row = await internalCertificate.get(access, { id: data.id });\n\t\tif (row.provider !== \"other\") {\n\t\t\tthrow new error.ValidationError(\"Cannot upload certificates for this type of provider\");\n\t\t}\n\n\t\tconst validations = await internalCertificate.validate(data);\n\t\tif (typeof validations.certificate === \"undefined\") {\n\t\t\tthrow new error.ValidationError(\"Certificate file was not provided\");\n\t\t}\n\n\t\t_.map(data.files, (file, name) => {\n\t\t\tif (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {\n\t\t\t\trow.meta[name] = file.data.toString();\n\t\t\t}\n\t\t});\n\n\t\tconst certificate = await internalCertificate.update(access, {\n\t\t\tid: data.id,\n\t\t\texpires_on: moment(validations.certificate.dates.to, \"X\").format(\"YYYY-MM-DD HH:mm:ss\"),\n\t\t\tdomain_names: [validations.certificate.cn],\n\t\t\tmeta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later\n\t\t});\n\n\t\tcertificate.meta = row.meta;\n\t\tawait internalCertificate.writeCustomCert(certificate);\n\t\treturn _.pick(row.meta, internalCertificate.allowedSslFiles);\n\t},\n\n\t/**\n\t * Uses the openssl command to validate the private key.\n\t * It will save the file to disk first, then run commands on it, then delete the file.\n\t *\n\t * @param {String}  privateKey    This is the entire key contents as a string\n\t */\n\tcheckPrivateKey: async (privateKey) => {\n\t\tconst filepath = await tempWrite(privateKey);\n\t\tconst failTimeout = setTimeout(() => {\n\t\t\tthrow new error.ValidationError(\n\t\t\t\t\"Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.\",\n\t\t\t);\n\t\t}, 10000);\n\n\t\ttry {\n\t\t\tconst result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `);\n\t\t\tclearTimeout(failTimeout);\n\t\t\tif (!result.toLowerCase().includes(\"key is valid\")) {\n\t\t\t\tthrow new error.ValidationError(`Result Validation Error: ${result}`);\n\t\t\t}\n\t\t\tfs.unlinkSync(filepath);\n\t\t\treturn true;\n\t\t} catch (err) {\n\t\t\tclearTimeout(failTimeout);\n\t\t\tfs.unlinkSync(filepath);\n\t\t\tthrow new error.ValidationError(`Certificate Key is not valid (${err.message})`, err);\n\t\t}\n\t},\n\n\t/**\n\t * Uses the openssl command to both validate and get info out of the certificate.\n\t * It will save the file to disk first, then run commands on it, then delete the file.\n\t *\n\t * @param {String}  certificate      This is the entire cert contents as a string\n\t * @param {Boolean} [throwExpired]  Throw when the certificate is out of date\n\t */\n\tgetCertificateInfo: async (certificate, throwExpired) => {\n\t\tconst filepath = await tempWrite(certificate);\n\t\ttry {\n\t\t\tconst certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired);\n\t\t\tfs.unlinkSync(filepath);\n\t\t\treturn certData;\n\t\t} catch (err) {\n\t\t\tfs.unlinkSync(filepath);\n\t\t\tthrow err;\n\t\t}\n\t},\n\n\t/**\n\t * Uses the openssl command to both validate and get info out of the certificate.\n\t * It will save the file to disk first, then run commands on it, then delete the file.\n\t *\n\t * @param {String}  certificateFile The file location on disk\n\t * @param {Boolean} [throw_expired]  Throw when the certificate is out of date\n\t */\n\tgetCertificateInfoFromFile: async (certificateFile, throw_expired) => {\n\t\tconst certData = {};\n\n\t\ttry {\n\t\t\tconst result = await utils.execFile(\"openssl\", [\"x509\", \"-in\", certificateFile, \"-subject\", \"-noout\"]);\n\t\t\t// Examples:\n\t\t\t// subject=CN = *.jc21.com\n\t\t\t// subject=CN = something.example.com\n\t\t\tconst regex = /(?:subject=)?[^=]+=\\s+(\\S+)/gim;\n\t\t\tconst match = regex.exec(result);\n\t\t\tif (match && typeof match[1] !== \"undefined\") {\n\t\t\t\tcertData.cn = match[1];\n\t\t\t}\n\n\t\t\tconst result2 = await utils.execFile(\"openssl\", [\"x509\", \"-in\", certificateFile, \"-issuer\", \"-noout\"]);\n\t\t\t// Examples:\n\t\t\t// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3\n\t\t\t// issuer=C = US, O = Let's Encrypt, CN = E5\n\t\t\t// issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA\",\"O = NginxProxyManager, CN = NginxProxyManager Intermediate CA\n\t\t\tconst regex2 = /^(?:issuer=)?(.*)$/gim;\n\t\t\tconst match2 = regex2.exec(result2);\n\t\t\tif (match2 && typeof match2[1] !== \"undefined\") {\n\t\t\t\tcertData.issuer = match2[1];\n\t\t\t}\n\n\t\t\tconst result3 = await utils.execFile(\"openssl\", [\"x509\", \"-in\", certificateFile, \"-dates\", \"-noout\"]);\n\t\t\t// notBefore=Jul 14 04:04:29 2018 GMT\n\t\t\t// notAfter=Oct 12 04:04:29 2018 GMT\n\t\t\tlet validFrom = null;\n\t\t\tlet validTo = null;\n\n\t\t\tconst lines = result3.split(\"\\n\");\n\t\t\tlines.map((str) => {\n\t\t\t\tconst regex = /^(\\S+)=(.*)$/gim;\n\t\t\t\tconst match = regex.exec(str.trim());\n\n\t\t\t\tif (match && typeof match[2] !== \"undefined\") {\n\t\t\t\t\tconst date = Number.parseInt(moment(match[2], \"MMM DD HH:mm:ss YYYY z\").format(\"X\"), 10);\n\n\t\t\t\t\tif (match[1].toLowerCase() === \"notbefore\") {\n\t\t\t\t\t\tvalidFrom = date;\n\t\t\t\t\t} else if (match[1].toLowerCase() === \"notafter\") {\n\t\t\t\t\t\tvalidTo = date;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tif (!validFrom || !validTo) {\n\t\t\t\tthrow new error.ValidationError(`Could not determine dates from certificate: ${result}`);\n\t\t\t}\n\n\t\t\tif (throw_expired && validTo < Number.parseInt(moment().format(\"X\"), 10)) {\n\t\t\t\tthrow new error.ValidationError(\"Certificate has expired\");\n\t\t\t}\n\n\t\t\tcertData.dates = {\n\t\t\t\tfrom: validFrom,\n\t\t\t\tto: validTo,\n\t\t\t};\n\n\t\t\treturn certData;\n\t\t} catch (err) {\n\t\t\tthrow new error.ValidationError(`Certificate is not valid (${err.message})`, err);\n\t\t}\n\t},\n\n\t/**\n\t * Cleans the ssl keys from the meta object and sets them to \"true\"\n\t *\n\t * @param   {Object}  meta\n\t * @param   {Boolean} [remove]\n\t * @returns {Object}\n\t */\n\tcleanMeta: (meta, remove) => {\n\t\tinternalCertificate.allowedSslFiles.map((key) => {\n\t\t\tif (typeof meta[key] !== \"undefined\" && meta[key]) {\n\t\t\t\tif (remove) {\n\t\t\t\t\tdelete meta[key];\n\t\t\t\t} else {\n\t\t\t\t\tmeta[key] = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\treturn meta;\n\t},\n\n\t/**\n\t * Request a certificate using the http challenge\n\t * @param   {Object}  certificate   the certificate row\n\t * @param   {String}  email         the email address to use for registration\n\t * @returns {Promise}\n\t */\n\trequestLetsEncryptSsl: async (certificate, email) => {\n\t\tlogger.info(\n\t\t\t`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(\", \")}`,\n\t\t);\n\n\t\tconst args = [\n\t\t\t\"certonly\",\n\t\t\t\"--config\",\n\t\t\tletsencryptConfig,\n\t\t\t\"--work-dir\",\n\t\t\tcertbotWorkDir,\n\t\t\t\"--logs-dir\",\n\t\t\tcertbotLogsDir,\n\t\t\t\"--cert-name\",\n\t\t\t`npm-${certificate.id}`,\n\t\t\t\"--agree-tos\",\n\t\t\t\"--authenticator\",\n\t\t\t\"webroot\",\n\t\t\t\"-m\",\n\t\t\temail,\n\t\t\t\"--preferred-challenges\",\n\t\t\t\"http\",\n\t\t\t\"--domains\",\n\t\t\tcertificate.domain_names.join(\",\"),\n\t\t];\n\n\t\t// Add key-type parameter if specified\n\t\tif (certificate.meta?.key_type) {\n\t\t\targs.push(\"--key-type\", certificate.meta.key_type);\n\t\t}\n\n\t\tconst adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);\n\t\targs.push(...adds.args);\n\n\t\tlogger.info(`Command: ${certbotCommand} ${args ? args.join(\" \") : \"\"}`);\n\n\t\tconst result = await utils.execFile(certbotCommand, args, adds.opts);\n\t\tlogger.success(result);\n\t\treturn result;\n\t},\n\n\t/**\n\t * @param   {Object}   certificate  the certificate row\n\t * @param   {String}   email        the email address to use for registration\n\t * @returns {Promise}\n\t */\n\trequestLetsEncryptSslWithDnsChallenge: async (certificate, email) => {\n\t\tawait installPlugin(certificate.meta.dns_provider);\n\t\tconst dnsPlugin = dnsPlugins[certificate.meta.dns_provider];\n\t\tlogger.info(\n\t\t\t`Requesting LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(\", \")}`,\n\t\t);\n\n\t\tconst credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;\n\t\tfs.mkdirSync(\"/etc/letsencrypt/credentials\", { recursive: true });\n\t\tfs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 });\n\n\t\t// Whether the plugin has a --<name>-credentials argument\n\t\tconst hasConfigArg = certificate.meta.dns_provider !== \"route53\";\n\n\t\tconst args = [\n\t\t\t\"certonly\",\n\t\t\t\"--config\",\n\t\t\tletsencryptConfig,\n\t\t\t\"--work-dir\",\n\t\t\tcertbotWorkDir,\n\t\t\t\"--logs-dir\",\n\t\t\tcertbotLogsDir,\n\t\t\t\"--cert-name\",\n\t\t\t`npm-${certificate.id}`,\n\t\t\t\"--agree-tos\",\n\t\t\t\"-m\",\n\t\t\temail,\n\t\t\t\"--preferred-challenges\",\n\t\t\t\"dns\",\n\t\t\t\"--domains\",\n\t\t\tcertificate.domain_names.join(\",\"),\n\t\t\t\"--authenticator\",\n\t\t\tdnsPlugin.full_plugin_name,\n\t\t];\n\n\t\tif (hasConfigArg) {\n\t\t\targs.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation);\n\t\t}\n\t\tif (certificate.meta.propagation_seconds !== undefined) {\n\t\t\targs.push(\n\t\t\t\t`--${dnsPlugin.full_plugin_name}-propagation-seconds`,\n\t\t\t\tcertificate.meta.propagation_seconds.toString(),\n\t\t\t);\n\t\t}\n\n\t\t// Add key-type parameter if specified\n\t\tif (certificate.meta?.key_type) {\n\t\t\targs.push(\"--key-type\", certificate.meta.key_type);\n\t\t}\n\n\t\tconst adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);\n\t\targs.push(...adds.args);\n\n\t\tlogger.info(`Command: ${certbotCommand} ${args ? args.join(\" \") : \"\"}`);\n\n\t\ttry {\n\t\t\tconst result = await utils.execFile(certbotCommand, args, adds.opts);\n\t\t\tlogger.info(result);\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\t// Don't fail if file does not exist, so no need for action in the callback\n\t\t\tfs.unlink(credentialsLocation, () => {});\n\t\t\tthrow err;\n\t\t}\n\t},\n\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @param   {Number}  data.id\n\t * @returns {Promise}\n\t */\n\trenew: async (access, data) => {\n\t\tawait access.can(\"certificates:update\", data);\n\t\tconst certificate = await internalCertificate.get(access, data);\n\n\t\tif (certificate.provider === \"letsencrypt\") {\n\t\t\tconst renewMethod = certificate.meta.dns_challenge\n\t\t\t\t? internalCertificate.renewLetsEncryptSslWithDnsChallenge\n\t\t\t\t: internalCertificate.renewLetsEncryptSsl;\n\n\t\t\tawait renewMethod(certificate);\n\t\t\tconst certInfo = await internalCertificate.getCertificateInfoFromFile(\n\t\t\t\t`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,\n\t\t\t);\n\n\t\t\tconst updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, {\n\t\t\t\texpires_on: moment(certInfo.dates.to, \"X\").format(\"YYYY-MM-DD HH:mm:ss\"),\n\t\t\t});\n\n\t\t\t// Add to audit log\n\t\t\tawait internalAuditLog.add(access, {\n\t\t\t\taction: \"renewed\",\n\t\t\t\tobject_type: \"certificate\",\n\t\t\t\tobject_id: updatedCertificate.id,\n\t\t\t\tmeta: updatedCertificate,\n\t\t\t});\n\n\t\t\treturn updatedCertificate;\n\t\t}\n\n\t\tthrow new error.ValidationError(\"Only Let'sEncrypt certificates can be renewed\");\n\t},\n\n\t/**\n\t * @param   {Object}  certificate   the certificate row\n\t * @returns {Promise}\n\t */\n\trenewLetsEncryptSsl: async (certificate) => {\n\t\tlogger.info(\n\t\t\t`Renewing LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(\", \")}`,\n\t\t);\n\n\t\tconst args = [\n\t\t\t\"renew\",\n\t\t\t\"--force-renewal\",\n\t\t\t\"--config\",\n\t\t\tletsencryptConfig,\n\t\t\t\"--work-dir\",\n\t\t\tcertbotWorkDir,\n\t\t\t\"--logs-dir\",\n\t\t\tcertbotLogsDir,\n\t\t\t\"--cert-name\",\n\t\t\t`npm-${certificate.id}`,\n\t\t\t\"--preferred-challenges\",\n\t\t\t\"http\",\n\t\t\t\"--no-random-sleep-on-renew\",\n\t\t\t\"--disable-hook-validation\",\n\t\t];\n\n\t\t// Add key-type parameter if specified\n\t\tif (certificate.meta?.key_type) {\n\t\t\targs.push(\"--key-type\", certificate.meta.key_type);\n\t\t}\n\n\t\tconst adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);\n\t\targs.push(...adds.args);\n\n\t\tlogger.info(`Command: ${certbotCommand} ${args ? args.join(\" \") : \"\"}`);\n\n\t\tconst result = await utils.execFile(certbotCommand, args, adds.opts);\n\t\tlogger.info(result);\n\t\treturn result;\n\t},\n\n\t/**\n\t * @param   {Object}  certificate   the certificate row\n\t * @returns {Promise}\n\t */\n\trenewLetsEncryptSslWithDnsChallenge: async (certificate) => {\n\t\tconst dnsPlugin = dnsPlugins[certificate.meta.dns_provider];\n\t\tif (!dnsPlugin) {\n\t\t\tthrow Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);\n\t\t}\n\n\t\tlogger.info(\n\t\t\t`Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(\", \")}`,\n\t\t);\n\n\t\tconst args = [\n\t\t\t\"renew\",\n\t\t\t\"--force-renewal\",\n\t\t\t\"--config\",\n\t\t\tletsencryptConfig,\n\t\t\t\"--work-dir\",\n\t\t\tcertbotWorkDir,\n\t\t\t\"--logs-dir\",\n\t\t\tcertbotLogsDir,\n\t\t\t\"--cert-name\",\n\t\t\t`npm-${certificate.id}`,\n\t\t\t\"--preferred-challenges\",\n\t\t\t\"dns\",\n\t\t\t\"--disable-hook-validation\",\n\t\t\t\"--no-random-sleep-on-renew\",\n\t\t];\n\n\t\t// Add key-type parameter if specified\n\t\tif (certificate.meta?.key_type) {\n\t\t\targs.push(\"--key-type\", certificate.meta.key_type);\n\t\t}\n\n\t\tconst adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);\n\t\targs.push(...adds.args);\n\n\t\tlogger.info(`Command: ${certbotCommand} ${args ? args.join(\" \") : \"\"}`);\n\n\t\tconst result = await utils.execFile(certbotCommand, args, adds.opts);\n\t\tlogger.info(result);\n\t\treturn result;\n\t},\n\n\t/**\n\t * @param   {Object}  certificate    the certificate row\n\t * @param   {Boolean} [throwErrors]\n\t * @returns {Promise}\n\t */\n\trevokeLetsEncryptSsl: async (certificate, throwErrors) => {\n\t\tlogger.info(\n\t\t\t`Revoking LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(\", \")}`,\n\t\t);\n\n\t\tconst args = [\n\t\t\t\"revoke\",\n\t\t\t\"--config\",\n\t\t\tletsencryptConfig,\n\t\t\t\"--work-dir\",\n\t\t\tcertbotWorkDir,\n\t\t\t\"--logs-dir\",\n\t\t\tcertbotLogsDir,\n\t\t\t\"--cert-path\",\n\t\t\t`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,\n\t\t\t\"--delete-after-revoke\",\n\t\t];\n\n\t\tconst adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);\n\t\targs.push(...adds.args);\n\n\t\tlogger.info(`Command: ${certbotCommand} ${args ? args.join(\" \") : \"\"}`);\n\n\t\ttry {\n\t\t\tconst result = await utils.execFile(certbotCommand, args, adds.opts);\n\t\t\tawait utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`);\n\t\t\tlogger.info(result);\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tlogger.error(err.message);\n\t\t\tif (throwErrors) {\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\t},\n\n\t/**\n\t * @param   {Object}  certificate\n\t * @returns {Boolean}\n\t */\n\thasLetsEncryptSslCerts: (certificate) => {\n\t\tconst letsencryptPath = internalCertificate.getLiveCertPath(certificate.id);\n\t\treturn fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`);\n\t},\n\n\t/**\n\t * @param   {Object}  inUseResult\n\t * @param   {Number}  inUseResult.total_count\n\t * @param   {Array}   inUseResult.proxy_hosts\n\t * @param   {Array}   inUseResult.redirection_hosts\n\t * @param   {Array}   inUseResult.dead_hosts\n\t * @returns {Promise}\n\t */\n\tdisableInUseHosts: async (inUseResult) => {\n\t\tif (inUseResult?.total_count) {\n\t\t\tif (inUseResult?.proxy_hosts.length) {\n\t\t\t\tawait internalNginx.bulkDeleteConfigs(\"proxy_host\", inUseResult.proxy_hosts);\n\t\t\t}\n\n\t\t\tif (inUseResult?.redirection_hosts.length) {\n\t\t\t\tawait internalNginx.bulkDeleteConfigs(\"redirection_host\", inUseResult.redirection_hosts);\n\t\t\t}\n\n\t\t\tif (inUseResult?.dead_hosts.length) {\n\t\t\t\tawait internalNginx.bulkDeleteConfigs(\"dead_host\", inUseResult.dead_hosts);\n\t\t\t}\n\t\t}\n\t},\n\n\t/**\n\t * @param   {Object}  inUseResult\n\t * @param   {Number}  inUseResult.total_count\n\t * @param   {Array}   inUseResult.proxy_hosts\n\t * @param   {Array}   inUseResult.redirection_hosts\n\t * @param   {Array}   inUseResult.dead_hosts\n\t * @returns {Promise}\n\t */\n\tenableInUseHosts: async (inUseResult) => {\n\t\tif (inUseResult.total_count) {\n\t\t\tif (inUseResult.proxy_hosts.length) {\n\t\t\t\tawait internalNginx.bulkGenerateConfigs(\"proxy_host\", inUseResult.proxy_hosts);\n\t\t\t}\n\n\t\t\tif (inUseResult.redirection_hosts.length) {\n\t\t\t\tawait internalNginx.bulkGenerateConfigs(\"redirection_host\", inUseResult.redirection_hosts);\n\t\t\t}\n\n\t\t\tif (inUseResult.dead_hosts.length) {\n\t\t\t\tawait internalNginx.bulkGenerateConfigs(\"dead_host\", inUseResult.dead_hosts);\n\t\t\t}\n\t\t}\n\t},\n\n\t/**\n\t *\n\t * @param   {Object}    payload\n\t * @param   {string[]}  payload.domains\n\t * @returns\n\t */\n\ttestHttpsChallenge: async (access, payload) => {\n\t\tawait access.can(\"certificates:list\");\n\n\t\t// Create a test challenge file\n\t\tconst testChallengeDir = \"/data/letsencrypt-acme-challenge/.well-known/acme-challenge\";\n\t\tconst testChallengeFile = `${testChallengeDir}/test-challenge`;\n\t\tfs.mkdirSync(testChallengeDir, { recursive: true });\n\t\tfs.writeFileSync(testChallengeFile, \"Success\", { encoding: \"utf8\" });\n\n\t\tconst results = {};\n\t\tfor (const domain of payload.domains) {\n\t\t\tresults[domain] = await internalCertificate.performTestForDomain(domain);\n\t\t}\n\n\t\t// Remove the test challenge file\n\t\tfs.unlinkSync(testChallengeFile);\n\n\t\treturn results;\n\t},\n\n\tperformTestForDomain: async (domain) => {\n\t\tlogger.info(`Testing http challenge for ${domain}`);\n\t\tconst agent = new ProxyAgent();\n\t\tconst url = `http://${domain}/.well-known/acme-challenge/test-challenge`;\n\t\tconst formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;\n\t\tconst options = {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"User-Agent\": \"Mozilla/5.0\",\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\"Content-Length\": Buffer.byteLength(formBody),\n\t\t\t},\n\t\t\tagent,\n\t\t};\n\n\t\tconst result = await new Promise((resolve) => {\n\t\t\tconst req = https.request(\"https://www.site24x7.com/tools/restapi-tester\", options, (res) => {\n\t\t\t\tlet responseBody = \"\";\n\n\t\t\t\tres.on(\"data\", (chunk) => {\n\t\t\t\t\tresponseBody = responseBody + chunk;\n\t\t\t\t});\n\n\t\t\t\tres.on(\"end\", () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst parsedBody = JSON.parse(`${responseBody}`);\n\t\t\t\t\t\tif (res.statusCode !== 200) {\n\t\t\t\t\t\t\tlogger.warn(\n\t\t\t\t\t\t\t\t`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresolve(parsedBody);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tif (res.statusCode !== 200) {\n\t\t\t\t\t\t\tlogger.warn(\n\t\t\t\t\t\t\t\t`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlogger.warn(\n\t\t\t\t\t\t\t\t`Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\n\t\t\t// Make sure to write the request body.\n\t\t\treq.write(formBody);\n\t\t\treq.end();\n\t\t\treq.on(\"error\", (e) => {\n\t\t\t\tlogger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);\n\t\t\t\tresolve(undefined);\n\t\t\t});\n\t\t});\n\n\t\tif (!result) {\n\t\t\t// Some error occurred while trying to get the data\n\t\t\treturn \"failed\";\n\t\t}\n\t\tif (result.error) {\n\t\t\tlogger.info(\n\t\t\t\t`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`,\n\t\t\t);\n\t\t\treturn `other:${result.error.msg}`;\n\t\t}\n\t\tif (`${result.responsecode}` === \"200\" && result.htmlresponse === \"Success\") {\n\t\t\t// Server exists and has responded with the correct data\n\t\t\treturn \"ok\";\n\t\t}\n\t\tif (`${result.responsecode}` === \"200\") {\n\t\t\t// Server exists but has responded with wrong data\n\t\t\tlogger.info(\n\t\t\t\t`HTTP challenge test failed for domain ${domain} because of invalid returned data:`,\n\t\t\t\tresult.htmlresponse,\n\t\t\t);\n\t\t\treturn \"wrong-data\";\n\t\t}\n\t\tif (`${result.responsecode}` === \"404\") {\n\t\t\t// Server exists but responded with a 404\n\t\t\tlogger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`);\n\t\t\treturn \"404\";\n\t\t}\n\t\tif (\n\t\t\t`${result.responsecode}` === \"0\" ||\n\t\t\t(typeof result.reason === \"string\" && result.reason.toLowerCase() === \"host unavailable\")\n\t\t) {\n\t\t\t// Server does not exist at domain\n\t\t\tlogger.info(`HTTP challenge test failed for domain ${domain} the host was not found`);\n\t\t\treturn \"no-host\";\n\t\t}\n\t\t// Other errors\n\t\tlogger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`);\n\t\treturn `other:${result.responsecode}`;\n\t},\n\n\tgetAdditionalCertbotArgs: (certificate_id, dns_provider) => {\n\t\tconst args = [];\n\t\tif (useLetsencryptServer() !== null) {\n\t\t\targs.push(\"--server\", useLetsencryptServer());\n\t\t}\n\t\tif (useLetsencryptStaging() && useLetsencryptServer() === null) {\n\t\t\targs.push(\"--staging\");\n\t\t}\n\n\t\t// For route53, add the credentials file as an environment variable,\n\t\t// inheriting the process env\n\t\tconst opts = {};\n\t\tif (certificate_id && dns_provider === \"route53\") {\n\t\t\topts.env = process.env;\n\t\t\topts.env.AWS_CONFIG_FILE = `/etc/letsencrypt/credentials/credentials-${certificate_id}`;\n\t\t}\n\n\t\tif (dns_provider === \"duckdns\") {\n\t\t\targs.push(\"--dns-duckdns-no-txt-restore\");\n\t\t}\n\n\t\treturn { args: args, opts: opts };\n\t},\n\n\tgetLiveCertPath: (certificateId) => {\n\t\treturn `/etc/letsencrypt/live/npm-${certificateId}`;\n\t},\n};\n\nexport default internalCertificate;\n"
  },
  {
    "path": "backend/internal/dead-host.js",
    "content": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport utils from \"../lib/utils.js\";\nimport deadHostModel from \"../models/dead_host.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalCertificate from \"./certificate.js\";\nimport internalHost from \"./host.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\"];\n};\n\nconst internalDeadHost = {\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: async (access, data) => {\n\t\tconst createCertificate = data.certificate_id === \"new\";\n\n\t\tif (createCertificate) {\n\t\t\tdelete data.certificate_id;\n\t\t}\n\n\t\tawait access.can(\"dead_hosts:create\", data);\n\n\t\t// Get a list of the domain names and check each of them against existing records\n\t\tconst domainNameCheckPromises = [];\n\n\t\tdata.domain_names.map((domain_name) => {\n\t\t\tdomainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name));\n\t\t\treturn true;\n\t\t});\n\n\t\tawait Promise.all(domainNameCheckPromises).then((check_results) => {\n\t\t\tcheck_results.map((result) => {\n\t\t\t\tif (result.is_taken) {\n\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t});\n\n\t\t// At this point the domains should have been checked\n\t\tdata.owner_user_id = access.token.getUserId(1);\n\t\tconst thisData = internalHost.cleanSslHstsData(data);\n\n\t\t// Fix for db field not having a default value\n\t\t// for this optional field.\n\t\tif (typeof data.advanced_config === \"undefined\") {\n\t\t\tthisData.advanced_config = \"\";\n\t\t}\n\n\t\tconst row = await deadHostModel.query()\n\t\t\t.insertAndFetch(thisData)\n\t\t\t.then(utils.omitRow(omissions()));\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"created\",\n\t\t\tobject_type: \"dead-host\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: thisData,\n\t\t});\n\n\t\tif (createCertificate) {\n\t\t\tconst cert = await internalCertificate.createQuickCertificate(access, data);\n\n\t\t\t// update host with cert id\n\t\t\tawait internalDeadHost.update(access, {\n\t\t\t\tid: row.id,\n\t\t\t\tcertificate_id: cert.id,\n\t\t\t});\n\t\t}\n\n\t\t// re-fetch with cert\n\t\tconst freshRow = await internalDeadHost.get(access, {\n\t\t\tid: row.id,\n\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t});\n\n\t\t// Sanity check\n\t\tif (createCertificate && !freshRow.certificate_id) {\n\t\t\tthrow new errs.InternalValidationError(\"The host was created but the Certificate creation failed.\");\n\t\t}\n\n\t\t// Configure nginx\n\t\tawait internalNginx.configure(deadHostModel, \"dead_host\", freshRow);\n\n\t\treturn freshRow;\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Number}  data.id\n\t * @return {Promise}\n\t */\n\tupdate: async (access, data) => {\n\t\tconst createCertificate = data.certificate_id === \"new\";\n\t\tif (createCertificate) {\n\t\t\tdelete data.certificate_id;\n\t\t}\n\n\t\tawait access.can(\"dead_hosts:update\", data.id);\n\n\t\t// Get a list of the domain names and check each of them against existing records\n\t\tconst domainNameCheckPromises = [];\n\t\tif (typeof data.domain_names !== \"undefined\") {\n\t\t\tdata.domain_names.map((domainName) => {\n\t\t\t\tdomainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, \"dead\", data.id));\n\t\t\t\treturn true;\n\t\t\t});\n\n\t\t\tconst checkResults = await Promise.all(domainNameCheckPromises);\n\t\t\tcheckResults.map((result) => {\n\t\t\t\tif (result.is_taken) {\n\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t\tconst row = await internalDeadHost.get(access, { id: data.id });\n\n\t\tif (row.id !== data.id) {\n\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t`404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`,\n\t\t\t);\n\t\t}\n\n\t\tif (createCertificate) {\n\t\t\tconst cert = await internalCertificate.createQuickCertificate(access, {\n\t\t\t\tdomain_names: data.domain_names || row.domain_names,\n\t\t\t\tmeta: _.assign({}, row.meta, data.meta),\n\t\t\t});\n\n\t\t\t// update host with cert id\n\t\t\tdata.certificate_id = cert.id;\n\t\t}\n\n\t\t// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.\n\t\tlet thisData = _.assign(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tdomain_names: row.domain_names,\n\t\t\t},\n\t\t\tdata,\n\t\t);\n\n\t\tthisData = internalHost.cleanSslHstsData(thisData, row);\n\n\n\t\t// do the row update\n\t\tawait deadHostModel\n\t\t\t.query()\n\t\t\t.where({id: data.id})\n\t\t\t.patch(data);\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"updated\",\n\t\t\tobject_type: \"dead-host\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: thisData,\n\t\t});\n\n\t\tconst thisRow = await internalDeadHost\n\t\t\t.get(access, {\n\t\t\t\tid: thisData.id,\n\t\t\t\texpand: [\"owner\", \"certificate\"],\n\t\t\t});\n\n\t\t// Configure nginx\n\t\tconst newMeta = await internalNginx.configure(deadHostModel, \"dead_host\", row);\n\t\trow.meta = newMeta;\n\t\treturn _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Number}   data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: async (access, data) => {\n\t\tconst accessData = await access.can(\"dead_hosts:get\", data.id);\n\t\tconst query = deadHostModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.andWhere(\"id\", data.id)\n\t\t\t.allowGraph(deadHostModel.defaultAllowGraph)\n\t\t\t.first();\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\tif (typeof data.expand !== \"undefined\" && data.expand !== null) {\n\t\t\tquery.withGraphFetched(`[${data.expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst row = await query.then(utils.omitRow(omissions()));\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\t\t// Custom omissions\n\t\tif (typeof data.omit !== \"undefined\" && data.omit !== null) {\n\t\t\treturn _.omit(row, data.omit);\n\t\t}\n\t\treturn row;\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: async (access, data) => {\n\t\tawait access.can(\"dead_hosts:delete\", data.id)\n\t\tconst row = await internalDeadHost.get(access, { id: data.id });\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\n\t\tawait deadHostModel\n\t\t\t.query()\n\t\t\t.where(\"id\", row.id)\n\t\t\t.patch({\n\t\t\t\tis_deleted: 1,\n\t\t\t});\n\n\t\t// Delete Nginx Config\n\t\tawait internalNginx.deleteConfig(\"dead_host\", row);\n\t\tawait internalNginx.reload();\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"deleted\",\n\t\t\tobject_type: \"dead-host\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(row, omissions()),\n\t\t});\n\t\treturn true;\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tenable: async (access, data) => {\n\t\tawait access.can(\"dead_hosts:update\", data.id)\n\t\tconst row = await internalDeadHost.get(access, {\n\t\t\tid: data.id,\n\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t});\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\t\tif (row.enabled) {\n\t\t\tthrow new errs.ValidationError(\"Host is already enabled\");\n\t\t}\n\n\t\trow.enabled = 1;\n\n\t\tawait deadHostModel\n\t\t\t.query()\n\t\t\t.where(\"id\", row.id)\n\t\t\t.patch({\n\t\t\t\tenabled: 1,\n\t\t\t});\n\n\t\t// Configure nginx\n\t\tawait internalNginx.configure(deadHostModel, \"dead_host\", row);\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"enabled\",\n\t\t\tobject_type: \"dead-host\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(row, omissions()),\n\t\t});\n\t\treturn true;\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdisable: async (access, data) => {\n\t\tawait access.can(\"dead_hosts:update\", data.id)\n\t\tconst row = await internalDeadHost.get(access, { id: data.id });\n\t\tif (!row || !row.id) {\n\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t}\n\t\tif (!row.enabled) {\n\t\t\tthrow new errs.ValidationError(\"Host is already disabled\");\n\t\t}\n\n\t\trow.enabled = 0;\n\n\t\tawait deadHostModel\n\t\t\t.query()\n\t\t\t.where(\"id\", row.id)\n\t\t\t.patch({\n\t\t\t\tenabled: 0,\n\t\t\t});\n\n\t\t// Delete Nginx Config\n\t\tawait internalNginx.deleteConfig(\"dead_host\", row);\n\t\tawait internalNginx.reload();\n\n\t\t// Add to audit log\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"disabled\",\n\t\t\tobject_type: \"dead-host\",\n\t\t\tobject_id: row.id,\n\t\t\tmeta: _.omit(row, omissions()),\n\t\t});\n\t\treturn true;\n\t},\n\n\t/**\n\t * All Hosts\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [searchQuery]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, searchQuery) => {\n\t\tconst accessData = await access.can(\"dead_hosts:list\")\n\t\tconst query = deadHostModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.groupBy(\"id\")\n\t\t\t.allowGraph(deadHostModel.defaultAllowGraph)\n\t\t\t.orderBy(castJsonIfNeed(\"domain_names\"), \"ASC\");\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\t// Query is used for searching\n\t\tif (typeof searchQuery === \"string\" && searchQuery.length > 0) {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(castJsonIfNeed(\"domain_names\"), \"like\", `%${searchQuery}%`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst rows = await query.then(utils.omitRows(omissions()));\n\t\tif (typeof expand !== \"undefined\" && expand !== null && expand.indexOf(\"certificate\") !== -1) {\n\t\t\tinternalHost.cleanAllRowsCertificateMeta(rows);\n\t\t}\n\t\treturn rows;\n\t},\n\n\t/**\n\t * Report use\n\t *\n\t * @param   {Number}  user_id\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: async (user_id, visibility) => {\n\t\tconst query = deadHostModel.query().count(\"id as count\").where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", user_id);\n\t\t}\n\n\t\tconst row = await query.first();\n\t\treturn Number.parseInt(row.count, 10);\n\t},\n};\n\nexport default internalDeadHost;\n"
  },
  {
    "path": "backend/internal/host.js",
    "content": "import _ from \"lodash\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport deadHostModel from \"../models/dead_host.js\";\nimport proxyHostModel from \"../models/proxy_host.js\";\nimport redirectionHostModel from \"../models/redirection_host.js\";\n\nconst internalHost = {\n\t/**\n\t * Makes sure that the ssl_* and hsts_* fields play nicely together.\n\t * ie: if there is no cert, then force_ssl is off.\n\t *     if force_ssl is off, then hsts_enabled is definitely off.\n\t *\n\t * @param   {object} data\n\t * @param   {object} [existing_data]\n\t * @returns {object}\n\t */\n\tcleanSslHstsData: (data, existingData) => {\n\t\tconst combinedData = _.assign({}, existingData || {}, data);\n\n\t\tif (!combinedData.certificate_id) {\n\t\t\tcombinedData.ssl_forced = false;\n\t\t\tcombinedData.http2_support = false;\n\t\t}\n\n\t\tif (!combinedData.ssl_forced) {\n\t\t\tcombinedData.hsts_enabled = false;\n\t\t}\n\n\t\tif (!combinedData.hsts_enabled) {\n\t\t\tcombinedData.hsts_subdomains = false;\n\t\t}\n\n\t\treturn combinedData;\n\t},\n\n\t/**\n\t * used by the getAll functions of hosts, this removes the certificate meta if present\n\t *\n\t * @param   {Array}  rows\n\t * @returns {Array}\n\t */\n\tcleanAllRowsCertificateMeta: (rows) => {\n\t\trows.map((_, idx) => {\n\t\t\tif (typeof rows[idx].certificate !== \"undefined\" && rows[idx].certificate) {\n\t\t\t\trows[idx].certificate.meta = {};\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\n\t\treturn rows;\n\t},\n\n\t/**\n\t * used by the get/update functions of hosts, this removes the certificate meta if present\n\t *\n\t * @param   {Object}  row\n\t * @returns {Object}\n\t */\n\tcleanRowCertificateMeta: (row) => {\n\t\tif (typeof row.certificate !== \"undefined\" && row.certificate) {\n\t\t\trow.certificate.meta = {};\n\t\t}\n\n\t\treturn row;\n\t},\n\n\t/**\n\t * This returns all the host types with any domain listed in the provided domainNames array.\n\t * This is used by the certificates to temporarily disable any host that is using the domain\n\t *\n\t * @param   {Array}  domainNames\n\t * @returns {Promise}\n\t */\n\tgetHostsWithDomains: async (domainNames) => {\n\t\tconst responseObject = {\n\t\t\ttotal_count: 0,\n\t\t\tdead_hosts: [],\n\t\t\tproxy_hosts: [],\n\t\t\tredirection_hosts: [],\n\t\t};\n\n\t\tconst proxyRes = await proxyHostModel.query().where(\"is_deleted\", 0);\n\t\tresponseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);\n\t\tresponseObject.total_count += responseObject.proxy_hosts.length;\n\n\t\tconst redirRes = await redirectionHostModel.query().where(\"is_deleted\", 0);\n\t\tresponseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);\n\t\tresponseObject.total_count += responseObject.redirection_hosts.length;\n\n\t\tconst deadRes = await deadHostModel.query().where(\"is_deleted\", 0);\n\t\tresponseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);\n\t\tresponseObject.total_count += responseObject.dead_hosts.length;\n\n\t\treturn responseObject;\n\t},\n\n\t/**\n\t * Internal use only, checks to see if the domain is already taken by any other record\n\t *\n\t * @param   {String}   hostname\n\t * @param   {String}   [ignore_type]   'proxy', 'redirection', 'dead'\n\t * @param   {Integer}  [ignore_id]     Must be supplied if type was also supplied\n\t * @returns {Promise}\n\t */\n\tisHostnameTaken: (hostname, ignore_type, ignore_id) => {\n\t\tconst promises = [\n\t\t\tproxyHostModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t.andWhere(castJsonIfNeed(\"domain_names\"), \"like\", `%${hostname}%`),\n\t\t\tredirectionHostModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t.andWhere(castJsonIfNeed(\"domain_names\"), \"like\", `%${hostname}%`),\n\t\t\tdeadHostModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t.andWhere(castJsonIfNeed(\"domain_names\"), \"like\", `%${hostname}%`),\n\t\t];\n\n\t\treturn Promise.all(promises).then((promises_results) => {\n\t\t\tlet is_taken = false;\n\n\t\t\tif (promises_results[0]) {\n\t\t\t\t// Proxy Hosts\n\t\t\t\tif (\n\t\t\t\t\tinternalHost._checkHostnameRecordsTaken(\n\t\t\t\t\t\thostname,\n\t\t\t\t\t\tpromises_results[0],\n\t\t\t\t\t\tignore_type === \"proxy\" && ignore_id ? ignore_id : 0,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tis_taken = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (promises_results[1]) {\n\t\t\t\t// Redirection Hosts\n\t\t\t\tif (\n\t\t\t\t\tinternalHost._checkHostnameRecordsTaken(\n\t\t\t\t\t\thostname,\n\t\t\t\t\t\tpromises_results[1],\n\t\t\t\t\t\tignore_type === \"redirection\" && ignore_id ? ignore_id : 0,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tis_taken = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (promises_results[2]) {\n\t\t\t\t// Dead Hosts\n\t\t\t\tif (\n\t\t\t\t\tinternalHost._checkHostnameRecordsTaken(\n\t\t\t\t\t\thostname,\n\t\t\t\t\t\tpromises_results[2],\n\t\t\t\t\t\tignore_type === \"dead\" && ignore_id ? ignore_id : 0,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tis_taken = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\thostname: hostname,\n\t\t\t\tis_taken: is_taken,\n\t\t\t};\n\t\t});\n\t},\n\n\t/**\n\t * Private call only\n\t *\n\t * @param   {String}  hostname\n\t * @param   {Array}   existingRows\n\t * @param   {Integer} [ignoreId]\n\t * @returns {Boolean}\n\t */\n\t_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {\n\t\tlet isTaken = false;\n\n\t\tif (existingRows?.length) {\n\t\t\texistingRows.map((existingRow) => {\n\t\t\t\texistingRow.domain_names.map((existingHostname) => {\n\t\t\t\t\t// Does this domain match?\n\t\t\t\t\tif (existingHostname.toLowerCase() === hostname.toLowerCase()) {\n\t\t\t\t\t\tif (!ignoreId || ignoreId !== existingRow.id) {\n\t\t\t\t\t\t\tisTaken = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\n\t\treturn isTaken;\n\t},\n\n\t/**\n\t * Private call only\n\t *\n\t * @param   {Array}   hosts\n\t * @param   {Array}   domainNames\n\t * @returns {Array}\n\t */\n\t_getHostsWithDomains: (hosts, domainNames) => {\n\t\tconst response = [];\n\n\t\tif (hosts?.length) {\n\t\t\thosts.map((host) => {\n\t\t\t\tlet hostMatches = false;\n\n\t\t\t\tdomainNames.map((domainName) => {\n\t\t\t\t\thost.domain_names.map((hostDomainName) => {\n\t\t\t\t\t\tif (domainName.toLowerCase() === hostDomainName.toLowerCase()) {\n\t\t\t\t\t\t\thostMatches = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\n\t\t\t\tif (hostMatches) {\n\t\t\t\t\tresponse.push(host);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\n\t\treturn response;\n\t},\n};\n\nexport default internalHost;\n"
  },
  {
    "path": "backend/internal/ip_ranges.js",
    "content": "import fs from \"node:fs\";\nimport https from \"node:https\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ProxyAgent } from \"proxy-agent\";\nimport errs from \"../lib/error.js\";\nimport utils from \"../lib/utils.js\";\nimport { ipRanges as logger } from \"../logger.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst CLOUDFRONT_URL = \"https://ip-ranges.amazonaws.com/ip-ranges.json\";\nconst CLOUDFARE_V4_URL = \"https://www.cloudflare.com/ips-v4\";\nconst CLOUDFARE_V6_URL = \"https://www.cloudflare.com/ips-v6\";\n\nconst regIpV4 = /^(\\d+\\.?){4}\\/\\d+/;\nconst regIpV6 = /^(([\\da-fA-F]+)?:)+\\/\\d+/;\n\nconst internalIpRanges = {\n\tinterval_timeout: 1000 * 60 * 60 * 6, // 6 hours\n\tinterval: null,\n\tinterval_processing: false,\n\titeration_count: 0,\n\n\tinitTimer: () => {\n\t\tlogger.info(\"IP Ranges Renewal Timer initialized\");\n\t\tinternalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);\n\t},\n\n\tfetchUrl: (url) => {\n\t\tconst agent = new ProxyAgent();\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlogger.info(`Fetching ${url}`);\n\t\t\treturn https\n\t\t\t\t.get(url, { agent }, (res) => {\n\t\t\t\t\tres.setEncoding(\"utf8\");\n\t\t\t\t\tlet raw_data = \"\";\n\t\t\t\t\tres.on(\"data\", (chunk) => {\n\t\t\t\t\t\traw_data += chunk;\n\t\t\t\t\t});\n\n\t\t\t\t\tres.on(\"end\", () => {\n\t\t\t\t\t\tresolve(raw_data);\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.on(\"error\", (err) => {\n\t\t\t\t\treject(err);\n\t\t\t\t});\n\t\t});\n\t},\n\n\t/**\n\t * Triggered at startup and then later by a timer, this will fetch the ip ranges from services and apply them to nginx.\n\t */\n\tfetch: () => {\n\t\tif (!internalIpRanges.interval_processing) {\n\t\t\tinternalIpRanges.interval_processing = true;\n\t\t\tlogger.info(\"Fetching IP Ranges from online services...\");\n\n\t\t\tlet ip_ranges = [];\n\n\t\t\treturn internalIpRanges\n\t\t\t\t.fetchUrl(CLOUDFRONT_URL)\n\t\t\t\t.then((cloudfront_data) => {\n\t\t\t\t\tconst data = JSON.parse(cloudfront_data);\n\n\t\t\t\t\tif (data && typeof data.prefixes !== \"undefined\") {\n\t\t\t\t\t\tdata.prefixes.map((item) => {\n\t\t\t\t\t\t\tif (item.service === \"CLOUDFRONT\") {\n\t\t\t\t\t\t\t\tip_ranges.push(item.ip_prefix);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (data && typeof data.ipv6_prefixes !== \"undefined\") {\n\t\t\t\t\t\tdata.ipv6_prefixes.map((item) => {\n\t\t\t\t\t\t\tif (item.service === \"CLOUDFRONT\") {\n\t\t\t\t\t\t\t\tip_ranges.push(item.ipv6_prefix);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\treturn internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);\n\t\t\t\t})\n\t\t\t\t.then((cloudfare_data) => {\n\t\t\t\t\tconst items = cloudfare_data.split(\"\\n\").filter((line) => regIpV4.test(line));\n\t\t\t\t\tip_ranges = [...ip_ranges, ...items];\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\treturn internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);\n\t\t\t\t})\n\t\t\t\t.then((cloudfare_data) => {\n\t\t\t\t\tconst items = cloudfare_data.split(\"\\n\").filter((line) => regIpV6.test(line));\n\t\t\t\t\tip_ranges = [...ip_ranges, ...items];\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\tconst clean_ip_ranges = [];\n\t\t\t\t\tip_ranges.map((range) => {\n\t\t\t\t\t\tif (range) {\n\t\t\t\t\t\t\tclean_ip_ranges.push(range);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn internalIpRanges.generateConfig(clean_ip_ranges).then(() => {\n\t\t\t\t\t\tif (internalIpRanges.iteration_count) {\n\t\t\t\t\t\t\t// Reload nginx\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\tinternalIpRanges.interval_processing = false;\n\t\t\t\t\tinternalIpRanges.iteration_count++;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlogger.fatal(err.message);\n\t\t\t\t\tinternalIpRanges.interval_processing = false;\n\t\t\t\t});\n\t\t}\n\t},\n\n\t/**\n\t * @param   {Array}  ip_ranges\n\t * @returns {Promise}\n\t */\n\tgenerateConfig: (ip_ranges) => {\n\t\tconst renderEngine = utils.getRenderEngine();\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet template = null;\n\t\t\tconst filename = \"/etc/nginx/conf.d/include/ip_ranges.conf\";\n\t\t\ttry {\n\t\t\t\ttemplate = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: \"utf8\" });\n\t\t\t} catch (err) {\n\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trenderEngine\n\t\t\t\t.parseAndRender(template, { ip_ranges: ip_ranges })\n\t\t\t\t.then((config_text) => {\n\t\t\t\t\tfs.writeFileSync(filename, config_text, { encoding: \"utf8\" });\n\t\t\t\t\tresolve(true);\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlogger.warn(`Could not write ${filename}: ${err.message}`);\n\t\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\t});\n\t\t});\n\t},\n};\n\nexport default internalIpRanges;\n"
  },
  {
    "path": "backend/internal/nginx.js",
    "content": "import fs from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport utils from \"../lib/utils.js\";\nimport { debug, nginx as logger } from \"../logger.js\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst internalNginx = {\n\t/**\n\t * This will:\n\t * - test the nginx config first to make sure it's OK\n\t * - create / recreate the config for the host\n\t * - test again\n\t * - IF OK:  update the meta with online status\n\t * - IF BAD: update the meta with offline status and remove the config entirely\n\t * - then reload nginx\n\t *\n\t * @param   {Object|String}  model\n\t * @param   {String}         host_type\n\t * @param   {Object}         host\n\t * @returns {Promise}\n\t */\n\tconfigure: (model, host_type, host) => {\n\t\tlet combined_meta = {};\n\n\t\treturn internalNginx\n\t\t\t.test()\n\t\t\t.then(() => {\n\t\t\t\t// Nginx is OK\n\t\t\t\t// We're deleting this config regardless.\n\t\t\t\t// Don't throw errors, as the file may not exist at all\n\t\t\t\t// Delete the .err file too\n\t\t\t\treturn internalNginx.deleteConfig(host_type, host, false, true);\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalNginx.generateConfig(host_type, host);\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\t// Test nginx again and update meta with result\n\t\t\t\treturn internalNginx\n\t\t\t\t\t.test()\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// nginx is ok\n\t\t\t\t\t\tcombined_meta = _.assign({}, host.meta, {\n\t\t\t\t\t\t\tnginx_online: true,\n\t\t\t\t\t\t\tnginx_err: null,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn model.query().where(\"id\", host.id).patch({\n\t\t\t\t\t\t\tmeta: combined_meta,\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.\n\t\t\t\t\t\t// It will always look like this:\n\t\t\t\t\t\t//   nginx: [alert] could not open error log file: open() \"/var/log/nginx/error.log\" failed (6: No such device or address)\n\n\t\t\t\t\t\tconst valid_lines = [];\n\t\t\t\t\t\tconst err_lines = err.message.split(\"\\n\");\n\t\t\t\t\t\terr_lines.map((line) => {\n\t\t\t\t\t\t\tif (line.indexOf(\"/var/log/nginx/error.log\") === -1) {\n\t\t\t\t\t\t\t\tvalid_lines.push(line);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tdebug(logger, \"Nginx test failed:\", valid_lines.join(\"\\n\"));\n\n\t\t\t\t\t\t// config is bad, update meta and delete config\n\t\t\t\t\t\tcombined_meta = _.assign({}, host.meta, {\n\t\t\t\t\t\t\tnginx_online: false,\n\t\t\t\t\t\t\tnginx_err: valid_lines.join(\"\\n\"),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn model\n\t\t\t\t\t\t\t.query()\n\t\t\t\t\t\t\t.where(\"id\", host.id)\n\t\t\t\t\t\t\t.patch({\n\t\t\t\t\t\t\t\tmeta: combined_meta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\tinternalNginx.renameConfigAsError(host_type, host);\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\treturn internalNginx.deleteConfig(host_type, host, true);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalNginx.reload();\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn combined_meta;\n\t\t\t});\n\t},\n\n\t/**\n\t * @returns {Promise}\n\t */\n\ttest: () => {\n\t\tdebug(logger, \"Testing Nginx configuration\");\n\t\treturn utils.execFile(\"/usr/sbin/nginx\", [\"-t\", \"-g\", \"error_log off;\"]);\n\t},\n\n\t/**\n\t * @returns {Promise}\n\t */\n\treload: () => {\n\t\treturn internalNginx.test().then(() => {\n\t\t\tlogger.info(\"Reloading Nginx\");\n\t\t\treturn utils.execFile(\"/usr/sbin/nginx\", [\"-s\", \"reload\"]);\n\t\t});\n\t},\n\n\t/**\n\t * @param   {String}  host_type\n\t * @param   {Integer} host_id\n\t * @returns {String}\n\t */\n\tgetConfigName: (host_type, host_id) => {\n\t\tif (host_type === \"default\") {\n\t\t\treturn \"/data/nginx/default_host/site.conf\";\n\t\t}\n\t\treturn `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`;\n\t},\n\n\t/**\n\t * Generates custom locations\n\t * @param   {Object}  host\n\t * @returns {Promise}\n\t */\n\trenderLocations: (host) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet template;\n\n\t\t\ttry {\n\t\t\t\ttemplate = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: \"utf8\" });\n\t\t\t} catch (err) {\n\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst renderEngine = utils.getRenderEngine();\n\t\t\tlet renderedLocations = \"\";\n\n\t\t\tconst locationRendering = async () => {\n\t\t\t\tfor (let i = 0; i < host.locations.length; i++) {\n\t\t\t\t\tconst locationCopy = Object.assign(\n\t\t\t\t\t\t{},\n\t\t\t\t\t\t{ access_list_id: host.access_list_id },\n\t\t\t\t\t\t{ certificate_id: host.certificate_id },\n\t\t\t\t\t\t{ ssl_forced: host.ssl_forced },\n\t\t\t\t\t\t{ caching_enabled: host.caching_enabled },\n\t\t\t\t\t\t{ block_exploits: host.block_exploits },\n\t\t\t\t\t\t{ allow_websocket_upgrade: host.allow_websocket_upgrade },\n\t\t\t\t\t\t{ http2_support: host.http2_support },\n\t\t\t\t\t\t{ hsts_enabled: host.hsts_enabled },\n\t\t\t\t\t\t{ hsts_subdomains: host.hsts_subdomains },\n\t\t\t\t\t\t{ access_list: host.access_list },\n\t\t\t\t\t\t{ certificate: host.certificate },\n\t\t\t\t\t\thost.locations[i],\n\t\t\t\t\t);\n\n\t\t\t\t\tif (locationCopy.forward_host.indexOf(\"/\") > -1) {\n\t\t\t\t\t\tconst splitted = locationCopy.forward_host.split(\"/\");\n\n\t\t\t\t\t\tlocationCopy.forward_host = splitted.shift();\n\t\t\t\t\t\tlocationCopy.forward_path = `/${splitted.join(\"/\")}`;\n\t\t\t\t\t}\n\n\t\t\t\t\trenderedLocations += await renderEngine.parseAndRender(template, locationCopy);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tlocationRendering().then(() => resolve(renderedLocations));\n\t\t});\n\t},\n\n\t/**\n\t * @param   {String}  host_type\n\t * @param   {Object}  host\n\t * @returns {Promise}\n\t */\n\tgenerateConfig: (host_type, host_row) => {\n\t\t// Prevent modifying the original object:\n\t\tconst host = JSON.parse(JSON.stringify(host_row));\n\t\tconst nice_host_type = internalNginx.getFileFriendlyHostType(host_type);\n\n\t\tdebug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2));\n\n\t\tconst renderEngine = utils.getRenderEngine();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet template = null;\n\t\t\tconst filename = internalNginx.getConfigName(nice_host_type, host.id);\n\n\t\t\ttry {\n\t\t\t\ttemplate = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: \"utf8\" });\n\t\t\t} catch (err) {\n\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet locationsPromise;\n\t\t\tlet origLocations;\n\n\t\t\t// Manipulate the data a bit before sending it to the template\n\t\t\tif (nice_host_type !== \"default\") {\n\t\t\t\thost.use_default_location = true;\n\t\t\t\tif (typeof host.advanced_config !== \"undefined\" && host.advanced_config) {\n\t\t\t\t\thost.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// For redirection hosts, if the scheme is not http or https, set it to $scheme\n\t\t\tif (nice_host_type === \"redirection_host\" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {\n\t\t\t\thost.forward_scheme = \"$scheme\";\n\t\t\t}\n\n\t\t\tif (host.locations) {\n\t\t\t\t//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));\n\t\t\t\torigLocations = [].concat(host.locations);\n\t\t\t\tlocationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {\n\t\t\t\t\thost.locations = renderedLocations;\n\t\t\t\t});\n\n\t\t\t\t// Allow someone who is using / custom location path to use it, and skip the default / location\n\t\t\t\t_.map(host.locations, (location) => {\n\t\t\t\t\tif (location.path === \"/\") {\n\t\t\t\t\t\thost.use_default_location = false;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tlocationsPromise = Promise.resolve();\n\t\t\t}\n\n\t\t\t// Set the IPv6 setting for the host\n\t\t\thost.ipv6 = internalNginx.ipv6Enabled();\n\n\t\t\tlocationsPromise.then(() => {\n\t\t\t\trenderEngine\n\t\t\t\t\t.parseAndRender(template, host)\n\t\t\t\t\t.then((config_text) => {\n\t\t\t\t\t\tfs.writeFileSync(filename, config_text, { encoding: \"utf8\" });\n\t\t\t\t\t\tdebug(logger, \"Wrote config:\", filename, config_text);\n\n\t\t\t\t\t\t// Restore locations array\n\t\t\t\t\t\thost.locations = origLocations;\n\n\t\t\t\t\t\tresolve(true);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tdebug(logger, `Could not write ${filename}:`, err.message);\n\t\t\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\t\t});\n\t\t\t});\n\t\t});\n\t},\n\n\t/**\n\t * This generates a temporary nginx config listening on port 80 for the domain names listed\n\t * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt\n\t * when requesting a certificate without having a hostname set up already.\n\t *\n\t * @param   {Object}  certificate\n\t * @returns {Promise}\n\t */\n\tgenerateLetsEncryptRequestConfig: (certificate) => {\n\t\tdebug(logger, \"Generating LetsEncrypt Request Config:\", certificate);\n\t\tconst renderEngine = utils.getRenderEngine();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet template = null;\n\t\t\tconst filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;\n\n\t\t\ttry {\n\t\t\t\ttemplate = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: \"utf8\" });\n\t\t\t} catch (err) {\n\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcertificate.ipv6 = internalNginx.ipv6Enabled();\n\n\t\t\trenderEngine\n\t\t\t\t.parseAndRender(template, certificate)\n\t\t\t\t.then((config_text) => {\n\t\t\t\t\tfs.writeFileSync(filename, config_text, { encoding: \"utf8\" });\n\t\t\t\t\tdebug(logger, \"Wrote config:\", filename, config_text);\n\t\t\t\t\tresolve(true);\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tdebug(logger, `Could not write ${filename}:`, err.message);\n\t\t\t\t\treject(new errs.ConfigurationError(err.message));\n\t\t\t\t});\n\t\t});\n\t},\n\n\t/**\n\t * A simple wrapper around unlinkSync that writes to the logger\n\t *\n\t * @param   {String}  filename\n\t */\n\tdeleteFile: (filename) => {\n\t\tif (!fs.existsSync(filename)) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tdebug(logger, `Deleting file: ${filename}`);\n\t\t\tfs.unlinkSync(filename);\n\t\t} catch (err) {\n\t\t\tdebug(logger, \"Could not delete file:\", JSON.stringify(err, null, 2));\n\t\t}\n\t},\n\n\t/**\n\t *\n\t * @param   {String} host_type\n\t * @returns String\n\t */\n\tgetFileFriendlyHostType: (host_type) => {\n\t\treturn host_type.replace(/-/g, \"_\");\n\t},\n\n\t/**\n\t * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`\n\t *\n\t * @param   {Object}  certificate\n\t * @returns {Promise}\n\t */\n\tdeleteLetsEncryptRequestConfig: (certificate) => {\n\t\tconst config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;\n\t\treturn new Promise((resolve /*, reject*/) => {\n\t\t\tinternalNginx.deleteFile(config_file);\n\t\t\tresolve();\n\t\t});\n\t},\n\n\t/**\n\t * @param   {String}  host_type\n\t * @param   {Object}  [host]\n\t * @param   {Boolean} [delete_err_file]\n\t * @returns {Promise}\n\t */\n\tdeleteConfig: (host_type, host, delete_err_file) => {\n\t\tconst config_file = internalNginx.getConfigName(\n\t\t\tinternalNginx.getFileFriendlyHostType(host_type),\n\t\t\ttypeof host === \"undefined\" ? 0 : host.id,\n\t\t);\n\t\tconst config_file_err = `${config_file}.err`;\n\n\t\treturn new Promise((resolve /*, reject*/) => {\n\t\t\tinternalNginx.deleteFile(config_file);\n\t\t\tif (delete_err_file) {\n\t\t\t\tinternalNginx.deleteFile(config_file_err);\n\t\t\t}\n\t\t\tresolve();\n\t\t});\n\t},\n\n\t/**\n\t * @param   {String}  host_type\n\t * @param   {Object}  [host]\n\t * @returns {Promise}\n\t */\n\trenameConfigAsError: (host_type, host) => {\n\t\tconst config_file = internalNginx.getConfigName(\n\t\t\tinternalNginx.getFileFriendlyHostType(host_type),\n\t\t\ttypeof host === \"undefined\" ? 0 : host.id,\n\t\t);\n\t\tconst config_file_err = `${config_file}.err`;\n\n\t\treturn new Promise((resolve /*, reject*/) => {\n\t\t\tfs.unlink(config_file, () => {\n\t\t\t\t// ignore result, continue\n\t\t\t\tfs.rename(config_file, config_file_err, () => {\n\t\t\t\t\t// also ignore result, as this is a debugging informative file anyway\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t},\n\n\t/**\n\t * @param   {String}  hostType\n\t * @param   {Array}   hosts\n\t * @returns {Promise}\n\t */\n\tbulkGenerateConfigs: (hostType, hosts) => {\n\t\tconst promises = [];\n\t\thosts.map((host) => {\n\t\t\tpromises.push(internalNginx.generateConfig(hostType, host));\n\t\t\treturn true;\n\t\t});\n\n\t\treturn Promise.all(promises);\n\t},\n\n\t/**\n\t * @param   {String}  host_type\n\t * @param   {Array}   hosts\n\t * @returns {Promise}\n\t */\n\tbulkDeleteConfigs: (host_type, hosts) => {\n\t\tconst promises = [];\n\t\thosts.map((host) => {\n\t\t\tpromises.push(internalNginx.deleteConfig(host_type, host, true));\n\t\t\treturn true;\n\t\t});\n\n\t\treturn Promise.all(promises);\n\t},\n\n\t/**\n\t * @param   {string}  config\n\t * @returns {boolean}\n\t */\n\tadvancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\\s*?location\\s*?\\/\\s*?{/im),\n\n\t/**\n\t * @returns {boolean}\n\t */\n\tipv6Enabled: () => {\n\t\tif (typeof process.env.DISABLE_IPV6 !== \"undefined\") {\n\t\t\tconst disabled = process.env.DISABLE_IPV6.toLowerCase();\n\t\t\treturn !(disabled === \"on\" || disabled === \"true\" || disabled === \"1\" || disabled === \"yes\");\n\t\t}\n\n\t\treturn true;\n\t},\n};\n\nexport default internalNginx;\n"
  },
  {
    "path": "backend/internal/proxy-host.js",
    "content": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport utils from \"../lib/utils.js\";\nimport proxyHostModel from \"../models/proxy_host.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalCertificate from \"./certificate.js\";\nimport internalHost from \"./host.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\", \"owner.is_deleted\"];\n};\n\nconst internalProxyHost = {\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: (access, data) => {\n\t\tlet thisData = data;\n\t\tconst createCertificate = thisData.certificate_id === \"new\";\n\n\t\tif (createCertificate) {\n\t\t\tdelete thisData.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:create\", thisData)\n\t\t\t.then(() => {\n\t\t\t\t// Get a list of the domain names and check each of them against existing records\n\t\t\t\tconst domain_name_check_promises = [];\n\n\t\t\t\tthisData.domain_names.map((domain_name) => {\n\t\t\t\t\tdomain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\n\t\t\t\treturn Promise.all(domain_name_check_promises).then((check_results) => {\n\t\t\t\t\tcheck_results.map((result) => {\n\t\t\t\t\t\tif (result.is_taken) {\n\t\t\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\t// At this point the domains should have been checked\n\t\t\t\tthisData.owner_user_id = access.token.getUserId(1);\n\t\t\t\tthisData = internalHost.cleanSslHstsData(thisData);\n\n\t\t\t\t// Fix for db field not having a default value\n\t\t\t\t// for this optional field.\n\t\t\t\tif (typeof thisData.advanced_config === \"undefined\") {\n\t\t\t\t\tthisData.advanced_config = \"\";\n\t\t\t\t}\n\n\t\t\t\treturn proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (createCertificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, thisData)\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\treturn internalProxyHost.update(access, {\n\t\t\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\t\t\tcertificate_id: cert.id,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// re-fetch with cert\n\t\t\t\treturn internalProxyHost.get(access, {\n\t\t\t\t\tid: row.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\", \"access_list.[clients,items]\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Configure nginx\n\t\t\t\treturn internalNginx.configure(proxyHostModel, \"proxy_host\", row).then(() => {\n\t\t\t\t\treturn row;\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Audit log\n\t\t\t\tthisData.meta = _.assign({}, thisData.meta || {}, row.meta);\n\n\t\t\t\t// Add to audit log\n\t\t\t\treturn internalAuditLog\n\t\t\t\t\t.add(access, {\n\t\t\t\t\t\taction: \"created\",\n\t\t\t\t\t\tobject_type: \"proxy-host\",\n\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\tmeta: thisData,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\treturn row;\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Number}  data.id\n\t * @return {Promise}\n\t */\n\tupdate: (access, data) => {\n\t\tlet thisData = data;\n\t\tconst createCertificate = thisData.certificate_id === \"new\";\n\n\t\tif (createCertificate) {\n\t\t\tdelete thisData.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:update\", thisData.id)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\t// Get a list of the domain names and check each of them against existing records\n\t\t\t\tconst domain_name_check_promises = [];\n\n\t\t\t\tif (typeof thisData.domain_names !== \"undefined\") {\n\t\t\t\t\tthisData.domain_names.map((domain_name) => {\n\t\t\t\t\t\treturn domain_name_check_promises.push(\n\t\t\t\t\t\t\tinternalHost.isHostnameTaken(domain_name, \"proxy\", thisData.id),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\n\t\t\t\t\treturn Promise.all(domain_name_check_promises).then((check_results) => {\n\t\t\t\t\t\tcheck_results.map((result) => {\n\t\t\t\t\t\t\tif (result.is_taken) {\n\t\t\t\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalProxyHost.get(access, { id: thisData.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row.id !== thisData.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (createCertificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, {\n\t\t\t\t\t\t\tdomain_names: thisData.domain_names || row.domain_names,\n\t\t\t\t\t\t\tmeta: _.assign({}, row.meta, thisData.meta),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\tthisData.certificate_id = cert.id;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.\n\t\t\t\tthisData = _.assign(\n\t\t\t\t\t{},\n\t\t\t\t\t{\n\t\t\t\t\t\tdomain_names: row.domain_names,\n\t\t\t\t\t},\n\t\t\t\t\tdata,\n\t\t\t\t);\n\n\t\t\t\tthisData = internalHost.cleanSslHstsData(thisData, row);\n\n\t\t\t\treturn proxyHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where({ id: thisData.id })\n\t\t\t\t\t.patch(thisData)\n\t\t\t\t\t.then(utils.omitRow(omissions()))\n\t\t\t\t\t.then((saved_row) => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog\n\t\t\t\t\t\t\t.add(access, {\n\t\t\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\t\t\tobject_type: \"proxy-host\",\n\t\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\t\tmeta: thisData,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\treturn saved_row;\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalProxyHost\n\t\t\t\t\t.get(access, {\n\t\t\t\t\t\tid: thisData.id,\n\t\t\t\t\t\texpand: [\"owner\", \"certificate\", \"access_list.[clients,items]\"],\n\t\t\t\t\t})\n\t\t\t\t\t.then((row) => {\n\t\t\t\t\t\tif (!row.enabled) {\n\t\t\t\t\t\t\t// No need to add nginx config if host is disabled\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Configure nginx\n\t\t\t\t\t\treturn internalNginx.configure(proxyHostModel, \"proxy_host\", row).then((new_meta) => {\n\t\t\t\t\t\t\trow.meta = new_meta;\n\t\t\t\t\t\t\treturn _.omit(internalHost.cleanRowCertificateMeta(row), omissions());\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Number}   data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: (access, data) => {\n\t\tconst thisData = data || {};\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:get\", thisData.id)\n\t\t\t.then((access_data) => {\n\t\t\t\tconst query = proxyHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.andWhere(\"id\", thisData.id)\n\t\t\t\t\t.allowGraph(proxyHostModel.defaultAllowGraph)\n\t\t\t\t\t.first();\n\n\t\t\t\tif (access_data.permission_visibility !== \"all\") {\n\t\t\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t\t\t}\n\n\t\t\t\tif (typeof thisData.expand !== \"undefined\" && thisData.expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${thisData.expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(thisData.id);\n\t\t\t\t}\n\t\t\t\tconst thisRow = internalHost.cleanRowCertificateMeta(row);\n\t\t\t\t// Custom omissions\n\t\t\t\tif (typeof thisData.omit !== \"undefined\" && thisData.omit !== null) {\n\t\t\t\t\treturn _.omit(row, thisData.omit);\n\t\t\t\t}\n\t\t\t\treturn thisRow;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:delete\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalProxyHost.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\n\t\t\t\treturn proxyHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tis_deleted: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"proxy_host\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"deleted\",\n\t\t\t\t\t\t\tobject_type: \"proxy-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tenable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalProxyHost.get(access, {\n\t\t\t\t\tid: data.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\", \"access_list\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Host is already enabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 1;\n\n\t\t\t\treturn proxyHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Configure nginx\n\t\t\t\t\t\treturn internalNginx.configure(proxyHostModel, \"proxy_host\", row);\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"enabled\",\n\t\t\t\t\t\t\tobject_type: \"proxy-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdisable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"proxy_hosts:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalProxyHost.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (!row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Host is already disabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 0;\n\n\t\t\t\treturn proxyHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 0,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"proxy_host\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"disabled\",\n\t\t\t\t\t\t\tobject_type: \"proxy-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * All Hosts\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [search_query]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, searchQuery) => {\n\t\tconst accessData = await access.can(\"proxy_hosts:list\");\n\n\t\tconst query = proxyHostModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.groupBy(\"id\")\n\t\t\t.allowGraph(proxyHostModel.defaultAllowGraph)\n\t\t\t.orderBy(castJsonIfNeed(\"domain_names\"), \"ASC\");\n\n\t\tif (accessData.permission_visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t}\n\n\t\t// Query is used for searching\n\t\tif (typeof searchQuery === \"string\" && searchQuery.length > 0) {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(castJsonIfNeed(\"domain_names\"), \"like\", `%${searchQuery}%`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst rows = await query.then(utils.omitRows(omissions()));\n\t\tif (typeof expand !== \"undefined\" && expand !== null && expand.indexOf(\"certificate\") !== -1) {\n\t\t\treturn internalHost.cleanAllRowsCertificateMeta(rows);\n\t\t}\n\t\treturn rows;\n\t},\n\n\t/**\n\t * Report use\n\t *\n\t * @param   {Number}  user_id\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: (user_id, visibility) => {\n\t\tconst query = proxyHostModel.query().count(\"id as count\").where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", user_id);\n\t\t}\n\n\t\treturn query.first().then((row) => {\n\t\t\treturn Number.parseInt(row.count, 10);\n\t\t});\n\t},\n};\n\nexport default internalProxyHost;\n"
  },
  {
    "path": "backend/internal/redirection-host.js",
    "content": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport utils from \"../lib/utils.js\";\nimport redirectionHostModel from \"../models/redirection_host.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalCertificate from \"./certificate.js\";\nimport internalHost from \"./host.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\"];\n};\n\nconst internalRedirectionHost = {\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: (access, data) => {\n\t\tlet thisData = data || {};\n\t\tconst createCertificate = thisData.certificate_id === \"new\";\n\n\t\tif (createCertificate) {\n\t\t\tdelete thisData.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:create\", thisData)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\t// Get a list of the domain names and check each of them against existing records\n\t\t\t\tconst domain_name_check_promises = [];\n\n\t\t\t\tthisData.domain_names.map((domain_name) => {\n\t\t\t\t\tdomain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\n\t\t\t\treturn Promise.all(domain_name_check_promises).then((check_results) => {\n\t\t\t\t\tcheck_results.map((result) => {\n\t\t\t\t\t\tif (result.is_taken) {\n\t\t\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\t// At this point the domains should have been checked\n\t\t\t\tthisData.owner_user_id = access.token.getUserId(1);\n\t\t\t\tthisData = internalHost.cleanSslHstsData(thisData);\n\n\t\t\t\t// Fix for db field not having a default value\n\t\t\t\t// for this optional field.\n\t\t\t\tif (typeof data.advanced_config === \"undefined\") {\n\t\t\t\t\tdata.advanced_config = \"\";\n\t\t\t\t}\n\n\t\t\t\treturn redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (createCertificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, thisData)\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\treturn internalRedirectionHost.update(access, {\n\t\t\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\t\t\tcertificate_id: cert.id,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// re-fetch with cert\n\t\t\t\treturn internalRedirectionHost.get(access, {\n\t\t\t\t\tid: row.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Configure nginx\n\t\t\t\treturn internalNginx.configure(redirectionHostModel, \"redirection_host\", row).then(() => {\n\t\t\t\t\treturn row;\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tthisData.meta = _.assign({}, thisData.meta || {}, row.meta);\n\n\t\t\t\t// Add to audit log\n\t\t\t\treturn internalAuditLog\n\t\t\t\t\t.add(access, {\n\t\t\t\t\t\taction: \"created\",\n\t\t\t\t\t\tobject_type: \"redirection-host\",\n\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\tmeta: thisData,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\treturn row;\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Number}  data.id\n\t * @return {Promise}\n\t */\n\tupdate: (access, data) => {\n\t\tlet thisData = data || {};\n\t\tconst createCertificate = thisData.certificate_id === \"new\";\n\n\t\tif (createCertificate) {\n\t\t\tdelete thisData.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:update\", thisData.id)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\t// Get a list of the domain names and check each of them against existing records\n\t\t\t\tconst domain_name_check_promises = [];\n\n\t\t\t\tif (typeof thisData.domain_names !== \"undefined\") {\n\t\t\t\t\tthisData.domain_names.map((domain_name) => {\n\t\t\t\t\t\tdomain_name_check_promises.push(\n\t\t\t\t\t\t\tinternalHost.isHostnameTaken(domain_name, \"redirection\", thisData.id),\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn Promise.all(domain_name_check_promises).then((check_results) => {\n\t\t\t\t\t\tcheck_results.map((result) => {\n\t\t\t\t\t\t\tif (result.is_taken) {\n\t\t\t\t\t\t\t\tthrow new errs.ValidationError(`${result.hostname} is already in use`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalRedirectionHost.get(access, { id: thisData.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row.id !== thisData.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (createCertificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, {\n\t\t\t\t\t\t\tdomain_names: thisData.domain_names || row.domain_names,\n\t\t\t\t\t\t\tmeta: _.assign({}, row.meta, thisData.meta),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\tthisData.certificate_id = cert.id;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.\n\t\t\t\tthisData = _.assign(\n\t\t\t\t\t{},\n\t\t\t\t\t{\n\t\t\t\t\t\tdomain_names: row.domain_names,\n\t\t\t\t\t},\n\t\t\t\t\tthisData,\n\t\t\t\t);\n\n\t\t\t\tthisData = internalHost.cleanSslHstsData(thisData, row);\n\n\t\t\t\treturn redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where({ id: thisData.id })\n\t\t\t\t\t.patch(thisData)\n\t\t\t\t\t.then((saved_row) => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog\n\t\t\t\t\t\t\t.add(access, {\n\t\t\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\t\t\tobject_type: \"redirection-host\",\n\t\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\t\tmeta: thisData,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\treturn _.omit(saved_row, omissions());\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalRedirectionHost\n\t\t\t\t\t.get(access, {\n\t\t\t\t\t\tid: thisData.id,\n\t\t\t\t\t\texpand: [\"owner\", \"certificate\"],\n\t\t\t\t\t})\n\t\t\t\t\t.then((row) => {\n\t\t\t\t\t\t// Configure nginx\n\t\t\t\t\t\treturn internalNginx\n\t\t\t\t\t\t\t.configure(redirectionHostModel, \"redirection_host\", row)\n\t\t\t\t\t\t\t.then((new_meta) => {\n\t\t\t\t\t\t\t\trow.meta = new_meta;\n\t\t\t\t\t\t\t\treturn _.omit(internalHost.cleanRowCertificateMeta(row), omissions());\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Number}   data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: (access, data) => {\n\t\tconst thisData = data || {};\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:get\", thisData.id)\n\t\t\t.then((access_data) => {\n\t\t\t\tconst query = redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.andWhere(\"id\", thisData.id)\n\t\t\t\t\t.allowGraph(redirectionHostModel.defaultAllowGraph)\n\t\t\t\t\t.first();\n\n\t\t\t\tif (access_data.permission_visibility !== \"all\") {\n\t\t\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t\t\t}\n\n\t\t\t\tif (typeof thisData.expand !== \"undefined\" && thisData.expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${thisData.expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tlet thisRow = row;\n\t\t\t\tif (!thisRow || !thisRow.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(thisData.id);\n\t\t\t\t}\n\t\t\t\tthisRow = internalHost.cleanRowCertificateMeta(thisRow);\n\t\t\t\t// Custom omissions\n\t\t\t\tif (typeof thisData.omit !== \"undefined\" && thisData.omit !== null) {\n\t\t\t\t\treturn _.omit(thisRow, thisData.omit);\n\t\t\t\t}\n\t\t\t\treturn thisRow;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:delete\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalRedirectionHost.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\n\t\t\t\treturn redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tis_deleted: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"redirection_host\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"deleted\",\n\t\t\t\t\t\t\tobject_type: \"redirection-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tenable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalRedirectionHost.get(access, {\n\t\t\t\t\tid: data.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Host is already enabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 1;\n\n\t\t\t\treturn redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Configure nginx\n\t\t\t\t\t\treturn internalNginx.configure(redirectionHostModel, \"redirection_host\", row);\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"enabled\",\n\t\t\t\t\t\t\tobject_type: \"redirection-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdisable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalRedirectionHost.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (!row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Host is already disabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 0;\n\n\t\t\t\treturn redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 0,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"redirection_host\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"disabled\",\n\t\t\t\t\t\t\tobject_type: \"redirection-host\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * All Hosts\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [search_query]\n\t * @returns {Promise}\n\t */\n\tgetAll: (access, expand, search_query) => {\n\t\treturn access\n\t\t\t.can(\"redirection_hosts:list\")\n\t\t\t.then((access_data) => {\n\t\t\t\tconst query = redirectionHostModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.groupBy(\"id\")\n\t\t\t\t\t.allowGraph(redirectionHostModel.defaultAllowGraph)\n\t\t\t\t\t.orderBy(castJsonIfNeed(\"domain_names\"), \"ASC\");\n\n\t\t\t\tif (access_data.permission_visibility !== \"all\") {\n\t\t\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t\t\t}\n\n\t\t\t\t// Query is used for searching\n\t\t\t\tif (typeof search_query === \"string\" && search_query.length > 0) {\n\t\t\t\t\tquery.where(function () {\n\t\t\t\t\t\tthis.where(castJsonIfNeed(\"domain_names\"), \"like\", `%${search_query}%`);\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRows(omissions()));\n\t\t\t})\n\t\t\t.then((rows) => {\n\t\t\t\tif (typeof expand !== \"undefined\" && expand !== null && expand.indexOf(\"certificate\") !== -1) {\n\t\t\t\t\treturn internalHost.cleanAllRowsCertificateMeta(rows);\n\t\t\t\t}\n\n\t\t\t\treturn rows;\n\t\t\t});\n\t},\n\n\t/**\n\t * Report use\n\t *\n\t * @param   {Number}  user_id\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: (user_id, visibility) => {\n\t\tconst query = redirectionHostModel.query().count(\"id as count\").where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", user_id);\n\t\t}\n\n\t\treturn query.first().then((row) => {\n\t\t\treturn Number.parseInt(row.count, 10);\n\t\t});\n\t},\n};\n\nexport default internalRedirectionHost;\n"
  },
  {
    "path": "backend/internal/remote-version.js",
    "content": "import https from \"node:https\";\nimport { ProxyAgent } from \"proxy-agent\";\nimport { debug, remoteVersion as logger } from \"../logger.js\";\nimport pjson from \"../package.json\" with { type: \"json\" };\n\nconst VERSION_URL = \"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest\";\n\nconst internalRemoteVersion = {\n\tcache_timeout: 1000 * 60 * 15, // 15 minutes\n\tlast_result: null,\n\tlast_fetch_time: null,\n\n\t/**\n\t * Fetch the latest version info, using a cached result if within the cache timeout period.\n\t * @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info\n\t */\n\tget: async () => {\n\t\tif (\n\t\t\t!internalRemoteVersion.last_result ||\n\t\t\t!internalRemoteVersion.last_fetch_time ||\n\t\t\tDate.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout\n\t\t) {\n\t\t\tconst raw = await internalRemoteVersion.fetchUrl(VERSION_URL);\n\t\t\tconst data = JSON.parse(raw);\n\t\t\tinternalRemoteVersion.last_result = data;\n\t\t\tinternalRemoteVersion.last_fetch_time = Date.now();\n\t\t} else {\n\t\t\tdebug(logger, \"Using cached remote version result\");\n\t\t}\n\n\t\tconst latestVersion = internalRemoteVersion.last_result.tag_name;\n\t\tconst version = pjson.version.split(\"-\").shift().split(\".\");\n\t\tconst currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;\n\t\treturn {\n\t\t\tcurrent: currentVersion,\n\t\t\tlatest: latestVersion,\n\t\t\tupdate_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),\n\t\t};\n\t},\n\n\tfetchUrl: (url) => {\n\t\tconst agent = new ProxyAgent();\n\t\tconst headers = {\n\t\t\t\"User-Agent\": `NginxProxyManager v${pjson.version}`,\n\t\t};\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlogger.info(`Fetching ${url}`);\n\t\t\treturn https\n\t\t\t\t.get(url, { agent, headers }, (res) => {\n\t\t\t\t\tres.setEncoding(\"utf8\");\n\t\t\t\t\tlet raw_data = \"\";\n\t\t\t\t\tres.on(\"data\", (chunk) => {\n\t\t\t\t\t\traw_data += chunk;\n\t\t\t\t\t});\n\t\t\t\t\tres.on(\"end\", () => {\n\t\t\t\t\t\tresolve(raw_data);\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.on(\"error\", (err) => {\n\t\t\t\t\treject(err);\n\t\t\t\t});\n\t\t});\n\t},\n\n\tcompareVersions: (current, latest) => {\n\t\tconst cleanCurrent = current.replace(/^v/, \"\");\n\t\tconst cleanLatest = latest.replace(/^v/, \"\");\n\n\t\tconst currentParts = cleanCurrent.split(\".\").map(Number);\n\t\tconst latestParts = cleanLatest.split(\".\").map(Number);\n\n\t\tfor (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {\n\t\t\tconst curr = currentParts[i] || 0;\n\t\t\tconst lat = latestParts[i] || 0;\n\n\t\t\tif (lat > curr) return true;\n\t\t\tif (lat < curr) return false;\n\t\t}\n\t\treturn false;\n\t},\n};\n\nexport default internalRemoteVersion;\n"
  },
  {
    "path": "backend/internal/report.js",
    "content": "import internalDeadHost from \"./dead-host.js\";\nimport internalProxyHost from \"./proxy-host.js\";\nimport internalRedirectionHost from \"./redirection-host.js\";\nimport internalStream from \"./stream.js\";\n\nconst internalReport = {\n\t/**\n\t * @param  {Access}   access\n\t * @return {Promise}\n\t */\n\tgetHostsReport: (access) => {\n\t\treturn access\n\t\t\t.can(\"reports:hosts\", 1)\n\t\t\t.then((access_data) => {\n\t\t\t\tconst userId = access.token.getUserId(1);\n\n\t\t\t\tconst promises = [\n\t\t\t\t\tinternalProxyHost.getCount(userId, access_data.permission_visibility),\n\t\t\t\t\tinternalRedirectionHost.getCount(userId, access_data.permission_visibility),\n\t\t\t\t\tinternalStream.getCount(userId, access_data.permission_visibility),\n\t\t\t\t\tinternalDeadHost.getCount(userId, access_data.permission_visibility),\n\t\t\t\t];\n\n\t\t\t\treturn Promise.all(promises);\n\t\t\t})\n\t\t\t.then((counts) => {\n\t\t\t\treturn {\n\t\t\t\t\tproxy: counts.shift(),\n\t\t\t\t\tredirection: counts.shift(),\n\t\t\t\t\tstream: counts.shift(),\n\t\t\t\t\tdead: counts.shift(),\n\t\t\t\t};\n\t\t\t});\n\t},\n};\n\nexport default internalReport;\n"
  },
  {
    "path": "backend/internal/setting.js",
    "content": "import fs from \"node:fs\";\nimport errs from \"../lib/error.js\";\nimport settingModel from \"../models/setting.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst internalSetting = {\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {String}  data.id\n\t * @return {Promise}\n\t */\n\tupdate: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"settings:update\", data.id)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\treturn internalSetting.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row.id !== data.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn settingModel.query().where({ id: data.id }).patch(data);\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalSetting.get(access, {\n\t\t\t\t\tid: data.id,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row.id === \"default-site\") {\n\t\t\t\t\t// write the html if we need to\n\t\t\t\t\tif (row.value === \"html\") {\n\t\t\t\t\t\tfs.writeFileSync(\"/data/nginx/default_www/index.html\", row.meta.html, { encoding: \"utf8\" });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Configure nginx\n\t\t\t\t\treturn internalNginx\n\t\t\t\t\t\t.deleteConfig(\"default\")\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn internalNginx.generateConfig(\"default\", row);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn internalNginx.test();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((/*err*/) => {\n\t\t\t\t\t\t\tinternalNginx\n\t\t\t\t\t\t\t\t.deleteConfig(\"default\")\n\t\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\t\treturn internalNginx.test();\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\t\t// I'm being slack here I know..\n\t\t\t\t\t\t\t\t\tthrow new errs.ValidationError(\"Could not reconfigure Nginx. Please check logs.\");\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {String}   data.id\n\t * @return {Promise}\n\t */\n\tget: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"settings:get\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn settingModel.query().where(\"id\", data.id).first();\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row) {\n\t\t\t\t\treturn row;\n\t\t\t\t}\n\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t});\n\t},\n\n\t/**\n\t * This will only count the settings\n\t *\n\t * @param   {Access}  access\n\t * @returns {*}\n\t */\n\tgetCount: (access) => {\n\t\treturn access\n\t\t\t.can(\"settings:list\")\n\t\t\t.then(() => {\n\t\t\t\treturn settingModel.query().count(\"id as count\").first();\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\treturn Number.parseInt(row.count, 10);\n\t\t\t});\n\t},\n\n\t/**\n\t * All settings\n\t *\n\t * @param   {Access}  access\n\t * @returns {Promise}\n\t */\n\tgetAll: (access) => {\n\t\treturn access.can(\"settings:list\").then(() => {\n\t\t\treturn settingModel.query().orderBy(\"description\", \"ASC\");\n\t\t});\n\t},\n};\n\nexport default internalSetting;\n"
  },
  {
    "path": "backend/internal/stream.js",
    "content": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport utils from \"../lib/utils.js\";\nimport streamModel from \"../models/stream.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalCertificate from \"./certificate.js\";\nimport internalHost from \"./host.js\";\nimport internalNginx from \"./nginx.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\", \"owner.is_deleted\", \"certificate.is_deleted\"];\n};\n\nconst internalStream = {\n\t/**\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: (access, data) => {\n\t\tconst create_certificate = data.certificate_id === \"new\";\n\n\t\tif (create_certificate) {\n\t\t\tdelete data.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"streams:create\", data)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\t// TODO: At this point the existing ports should have been checked\n\t\t\t\tdata.owner_user_id = access.token.getUserId(1);\n\n\t\t\t\tif (typeof data.meta === \"undefined\") {\n\t\t\t\t\tdata.meta = {};\n\t\t\t\t}\n\n\t\t\t\t// streams aren't routed by domain name so don't store domain names in the DB\n\t\t\t\tconst data_no_domains = structuredClone(data);\n\t\t\t\tdelete data_no_domains.domain_names;\n\n\t\t\t\treturn streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (create_certificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, data)\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\treturn internalStream.update(access, {\n\t\t\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\t\t\tcertificate_id: cert.id,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// re-fetch with cert\n\t\t\t\treturn internalStream.get(access, {\n\t\t\t\t\tid: row.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Configure nginx\n\t\t\t\treturn internalNginx.configure(streamModel, \"stream\", row).then(() => {\n\t\t\t\t\treturn row;\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Add to audit log\n\t\t\t\treturn internalAuditLog\n\t\t\t\t\t.add(access, {\n\t\t\t\t\t\taction: \"created\",\n\t\t\t\t\t\tobject_type: \"stream\",\n\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\tmeta: data,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\treturn row;\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Number}  data.id\n\t * @return {Promise}\n\t */\n\tupdate: (access, data) => {\n\t\tlet thisData = data;\n\t\tconst create_certificate = thisData.certificate_id === \"new\";\n\n\t\tif (create_certificate) {\n\t\t\tdelete thisData.certificate_id;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"streams:update\", thisData.id)\n\t\t\t.then((/*access_data*/) => {\n\t\t\t\t// TODO: at this point the existing streams should have been checked\n\t\t\t\treturn internalStream.get(access, { id: thisData.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (row.id !== thisData.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (create_certificate) {\n\t\t\t\t\treturn internalCertificate\n\t\t\t\t\t\t.createQuickCertificate(access, {\n\t\t\t\t\t\t\tdomain_names: thisData.domain_names || row.domain_names,\n\t\t\t\t\t\t\tmeta: _.assign({}, row.meta, thisData.meta),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then((cert) => {\n\t\t\t\t\t\t\t// update host with cert id\n\t\t\t\t\t\t\tthisData.certificate_id = cert.id;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn row;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn row;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\t// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.\n\t\t\t\tthisData = _.assign(\n\t\t\t\t\t{},\n\t\t\t\t\t{\n\t\t\t\t\t\tdomain_names: row.domain_names,\n\t\t\t\t\t},\n\t\t\t\t\tthisData,\n\t\t\t\t);\n\n\t\t\t\treturn streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.patchAndFetchById(row.id, thisData)\n\t\t\t\t\t.then(utils.omitRow(omissions()))\n\t\t\t\t\t.then((saved_row) => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog\n\t\t\t\t\t\t\t.add(access, {\n\t\t\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\t\t\tobject_type: \"stream\",\n\t\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\t\tmeta: thisData,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\treturn saved_row;\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalStream.get(access, { id: thisData.id, expand: [\"owner\", \"certificate\"] }).then((row) => {\n\t\t\t\t\treturn internalNginx.configure(streamModel, \"stream\", row).then((new_meta) => {\n\t\t\t\t\t\trow.meta = new_meta;\n\t\t\t\t\t\treturn _.omit(internalHost.cleanRowCertificateMeta(row), omissions());\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   data\n\t * @param  {Number}   data.id\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: (access, data) => {\n\t\tconst thisData = data || {};\n\t\treturn access\n\t\t\t.can(\"streams:get\", thisData.id)\n\t\t\t.then((access_data) => {\n\t\t\t\tconst query = streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.andWhere(\"id\", thisData.id)\n\t\t\t\t\t.allowGraph(streamModel.defaultAllowGraph)\n\t\t\t\t\t.first();\n\n\t\t\t\tif (access_data.permission_visibility !== \"all\") {\n\t\t\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t\t\t}\n\n\t\t\t\tif (typeof thisData.expand !== \"undefined\" && thisData.expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${thisData.expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tlet thisRow = row;\n\t\t\t\tif (!thisRow || !thisRow.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(thisData.id);\n\t\t\t\t}\n\t\t\t\tthisRow = internalHost.cleanRowCertificateMeta(thisRow);\n\t\t\t\t// Custom omissions\n\t\t\t\tif (typeof thisData.omit !== \"undefined\" && thisData.omit !== null) {\n\t\t\t\t\treturn _.omit(thisRow, thisData.omit);\n\t\t\t\t}\n\t\t\t\treturn thisRow;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"streams:delete\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalStream.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\n\t\t\t\treturn streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tis_deleted: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"stream\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"deleted\",\n\t\t\t\t\t\t\tobject_type: \"stream\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tenable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"streams:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalStream.get(access, {\n\t\t\t\t\tid: data.id,\n\t\t\t\t\texpand: [\"certificate\", \"owner\"],\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Stream is already enabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 1;\n\n\t\t\t\treturn streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Configure nginx\n\t\t\t\t\t\treturn internalNginx.configure(streamModel, \"stream\", row);\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"enabled\",\n\t\t\t\t\t\t\tobject_type: \"stream\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Number}  data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdisable: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"streams:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalStream.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\t\t\t\tif (!row.enabled) {\n\t\t\t\t\tthrow new errs.ValidationError(\"Stream is already disabled\");\n\t\t\t\t}\n\n\t\t\t\trow.enabled = 0;\n\n\t\t\t\treturn streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", row.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tenabled: 0,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Delete Nginx Config\n\t\t\t\t\t\treturn internalNginx.deleteConfig(\"stream\", row).then(() => {\n\t\t\t\t\t\t\treturn internalNginx.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"disabled\",\n\t\t\t\t\t\t\tobject_type: \"stream\",\n\t\t\t\t\t\t\tobject_id: row.id,\n\t\t\t\t\t\t\tmeta: _.omit(row, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * All Streams\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [search_query]\n\t * @returns {Promise}\n\t */\n\tgetAll: (access, expand, search_query) => {\n\t\treturn access\n\t\t\t.can(\"streams:list\")\n\t\t\t.then((access_data) => {\n\t\t\t\tconst query = streamModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.groupBy(\"id\")\n\t\t\t\t\t.allowGraph(streamModel.defaultAllowGraph)\n\t\t\t\t\t.orderBy(\"incoming_port\", \"ASC\");\n\n\t\t\t\tif (access_data.permission_visibility !== \"all\") {\n\t\t\t\t\tquery.andWhere(\"owner_user_id\", access.token.getUserId(1));\n\t\t\t\t}\n\n\t\t\t\t// Query is used for searching\n\t\t\t\tif (typeof search_query === \"string\" && search_query.length > 0) {\n\t\t\t\t\tquery.where(function () {\n\t\t\t\t\t\tthis.where(castJsonIfNeed(\"incoming_port\"), \"like\", `%${search_query}%`);\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRows(omissions()));\n\t\t\t})\n\t\t\t.then((rows) => {\n\t\t\t\tif (typeof expand !== \"undefined\" && expand !== null && expand.indexOf(\"certificate\") !== -1) {\n\t\t\t\t\treturn internalHost.cleanAllRowsCertificateMeta(rows);\n\t\t\t\t}\n\n\t\t\t\treturn rows;\n\t\t\t});\n\t},\n\n\t/**\n\t * Report use\n\t *\n\t * @param   {Number}  user_id\n\t * @param   {String}  visibility\n\t * @returns {Promise}\n\t */\n\tgetCount: (user_id, visibility) => {\n\t\tconst query = streamModel.query().count(\"id AS count\").where(\"is_deleted\", 0);\n\n\t\tif (visibility !== \"all\") {\n\t\t\tquery.andWhere(\"owner_user_id\", user_id);\n\t\t}\n\n\t\treturn query.first().then((row) => {\n\t\t\treturn Number.parseInt(row.count, 10);\n\t\t});\n\t},\n};\n\nexport default internalStream;\n"
  },
  {
    "path": "backend/internal/token.js",
    "content": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { parseDatePeriod } from \"../lib/helpers.js\";\nimport authModel from \"../models/auth.js\";\nimport TokenModel from \"../models/token.js\";\nimport userModel from \"../models/user.js\";\nimport twoFactor from \"./2fa.js\";\n\nconst ERROR_MESSAGE_INVALID_AUTH = \"Invalid email or password\";\nconst ERROR_MESSAGE_INVALID_AUTH_I18N = \"error.invalid-auth\";\nconst ERROR_MESSAGE_INVALID_2FA = \"Invalid verification code\";\nconst ERROR_MESSAGE_INVALID_2FA_I18N = \"error.invalid-2fa\";\n\nexport default {\n\t/**\n\t * @param   {Object} data\n\t * @param   {String} data.identity\n\t * @param   {String} data.secret\n\t * @param   {String} [data.scope]\n\t * @param   {String} [data.expiry]\n\t * @param   {String} [issuer]\n\t * @returns {Promise}\n\t */\n\tgetTokenFromEmail: async (data, issuer) => {\n\t\tconst Token = TokenModel();\n\n\t\tdata.scope = data.scope || \"user\";\n\t\tdata.expiry = data.expiry || \"1d\";\n\n\t\tconst user = await userModel\n\t\t\t.query()\n\t\t\t.where(\"email\", data.identity.toLowerCase().trim())\n\t\t\t.andWhere(\"is_deleted\", 0)\n\t\t\t.andWhere(\"is_disabled\", 0)\n\t\t\t.first();\n\n\t\tif (!user) {\n\t\t\tthrow new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);\n\t\t}\n\n\t\tconst auth = await authModel\n\t\t\t.query()\n\t\t\t.where(\"user_id\", \"=\", user.id)\n\t\t\t.where(\"type\", \"=\", \"password\")\n\t\t\t.first();\n\n\t\tif (!auth) {\n\t\t\tthrow new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);\n\t\t}\n\n\t\tconst valid = await auth.verifyPassword(data.secret);\n\t\tif (!valid) {\n\t\t\tthrow new errs.AuthError(\n\t\t\t\tERROR_MESSAGE_INVALID_AUTH,\n\t\t\t\tERROR_MESSAGE_INVALID_AUTH_I18N,\n\t\t\t);\n\t\t}\n\n\t\tif (data.scope !== \"user\" && _.indexOf(user.roles, data.scope) === -1) {\n\t\t\t// The scope requested doesn't exist as a role against the user,\n\t\t\t// you shall not pass.\n\t\t\tthrow new errs.AuthError(`Invalid scope: ${data.scope}`);\n\t\t}\n\n\t\t// Check if 2FA is enabled\n\t\tconst has2FA = await twoFactor.isEnabled(user.id);\n\t\tif (has2FA) {\n\t\t\t// Return challenge token instead of full token\n\t\t\tconst challengeToken = await Token.create({\n\t\t\t\tiss: issuer || \"api\",\n\t\t\t\tattrs: {\n\t\t\t\t\tid: user.id,\n\t\t\t\t},\n\t\t\t\tscope: [\"2fa-challenge\"],\n\t\t\t\texpiresIn: \"5m\",\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\trequires_2fa: true,\n\t\t\t\tchallenge_token: challengeToken.token,\n\t\t\t};\n\t\t}\n\n\t\t// Create a moment of the expiry expression\n\t\tconst expiry = parseDatePeriod(data.expiry);\n\t\tif (expiry === null) {\n\t\t\tthrow new errs.AuthError(`Invalid expiry time: ${data.expiry}`);\n\t\t}\n\n\t\tconst signed = await Token.create({\n\t\t\tiss: issuer || \"api\",\n\t\t\tattrs: {\n\t\t\t\tid: user.id,\n\t\t\t},\n\t\t\tscope: [data.scope],\n\t\t\texpiresIn: data.expiry,\n\t\t});\n\n\t\treturn {\n\t\t\ttoken: signed.token,\n\t\t\texpires: expiry.toISOString(),\n\t\t};\n\t},\n\n\t/**\n\t * @param {Access} access\n\t * @param {Object} [data]\n\t * @param {String} [data.expiry]\n\t * @param {String} [data.scope]   Only considered if existing token scope is admin\n\t * @returns {Promise}\n\t */\n\tgetFreshToken: async (access, data) => {\n\t\tconst Token = TokenModel();\n\t\tconst thisData = data || {};\n\n\t\tthisData.expiry = thisData.expiry || \"1d\";\n\n\t\tif (access?.token.getUserId(0)) {\n\t\t\t// Create a moment of the expiry expression\n\t\t\tconst expiry = parseDatePeriod(thisData.expiry);\n\t\t\tif (expiry === null) {\n\t\t\t\tthrow new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);\n\t\t\t}\n\n\t\t\tconst token_attrs = {\n\t\t\t\tid: access.token.getUserId(0),\n\t\t\t};\n\n\t\t\t// Only admins can request otherwise scoped tokens\n\t\t\tlet scope = access.token.get(\"scope\");\n\t\t\tif (thisData.scope && access.token.hasScope(\"admin\")) {\n\t\t\t\tscope = [thisData.scope];\n\n\t\t\t\tif (thisData.scope === \"job-board\" || thisData.scope === \"worker\") {\n\t\t\t\t\ttoken_attrs.id = 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst signed = await Token.create({\n\t\t\t\tiss: \"api\",\n\t\t\t\tscope: scope,\n\t\t\t\tattrs: token_attrs,\n\t\t\t\texpiresIn: thisData.expiry,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\ttoken: signed.token,\n\t\t\t\texpires: expiry.toISOString(),\n\t\t\t};\n\t\t}\n\t\tthrow new error.AssertionFailedError(\"Existing token contained invalid user data\");\n\t},\n\n\t/**\n\t * Verify 2FA code and return full token\n\t * @param {string} challengeToken\n\t * @param {string} code\n\t * @param {string} [expiry]\n\t * @returns {Promise}\n\t */\n\tverify2FA: async (challengeToken, code, expiry) => {\n\t\tconst Token = TokenModel();\n\t\tconst tokenExpiry = expiry || \"1d\";\n\n\t\t// Verify challenge token\n\t\tlet tokenData;\n\t\ttry {\n\t\t\ttokenData = await Token.load(challengeToken);\n\t\t} catch {\n\t\t\tthrow new errs.AuthError(\"Invalid or expired challenge token\");\n\t\t}\n\n\t\t// Check scope\n\t\tif (!tokenData.scope || tokenData.scope[0] !== \"2fa-challenge\") {\n\t\t\tthrow new errs.AuthError(\"Invalid challenge token\");\n\t\t}\n\n\t\tconst userId = tokenData.attrs?.id;\n\t\tif (!userId) {\n\t\t\tthrow new errs.AuthError(\"Invalid challenge token\");\n\t\t}\n\n\t\t// Verify 2FA code\n\t\tconst valid = await twoFactor.verifyForLogin(userId, code);\n\t\tif (!valid) {\n\t\t\tthrow new errs.AuthError(\n\t\t\t\tERROR_MESSAGE_INVALID_2FA,\n\t\t\t\tERROR_MESSAGE_INVALID_2FA_I18N,\n\t\t\t);\n\t\t}\n\n\t\t// Create full token\n\t\tconst expiryDate = parseDatePeriod(tokenExpiry);\n\t\tif (expiryDate === null) {\n\t\t\tthrow new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);\n\t\t}\n\n\t\tconst signed = await Token.create({\n\t\t\tiss: \"api\",\n\t\t\tattrs: {\n\t\t\t\tid: userId,\n\t\t\t},\n\t\t\tscope: [\"user\"],\n\t\t\texpiresIn: tokenExpiry,\n\t\t});\n\n\t\treturn {\n\t\t\ttoken: signed.token,\n\t\t\texpires: expiryDate.toISOString(),\n\t\t};\n\t},\n\n\t/**\n\t * @param   {Object} user\n\t * @returns {Promise}\n\t */\n\tgetTokenFromUser: async (user) => {\n\t\tconst expire = \"1d\";\n\t\tconst Token = TokenModel();\n\t\tconst expiry = parseDatePeriod(expire);\n\n\t\tconst signed = await Token.create({\n\t\t\tiss: \"api\",\n\t\t\tattrs: {\n\t\t\t\tid: user.id,\n\t\t\t},\n\t\t\tscope: [\"user\"],\n\t\t\texpiresIn: expire,\n\t\t});\n\n\t\treturn {\n\t\t\ttoken: signed.token,\n\t\t\texpires: expiry.toISOString(),\n\t\t\tuser: user,\n\t\t};\n\t},\n};\n"
  },
  {
    "path": "backend/internal/user.js",
    "content": "import gravatar from \"gravatar\";\nimport _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport utils from \"../lib/utils.js\";\nimport authModel from \"../models/auth.js\";\nimport userModel from \"../models/user.js\";\nimport userPermissionModel from \"../models/user_permission.js\";\nimport internalAuditLog from \"./audit-log.js\";\nimport internalToken from \"./token.js\";\n\nconst omissions = () => {\n\treturn [\"is_deleted\", \"permissions.id\", \"permissions.user_id\", \"permissions.created_on\", \"permissions.modified_on\"];\n};\n\nconst DEFAULT_AVATAR = gravatar.url(\"admin@example.com\", { default: \"mm\" });\n\nconst internalUser = {\n\t/**\n\t * Create a user can happen unauthenticated only once and only when no active users exist.\n\t * Otherwise, a valid auth method is required.\n\t *\n\t * @param   {Access}  access\n\t * @param   {Object}  data\n\t * @returns {Promise}\n\t */\n\tcreate: async (access, data) => {\n\t\tconst auth = data.auth || null;\n\t\tdelete data.auth;\n\n\t\tdata.avatar = data.avatar || \"\";\n\t\tdata.roles = data.roles || [];\n\n\t\tif (typeof data.is_disabled !== \"undefined\") {\n\t\t\tdata.is_disabled = data.is_disabled ? 1 : 0;\n\t\t}\n\n\t\tawait access.can(\"users:create\", data);\n\t\tdata.avatar = gravatar.url(data.email, { default: \"mm\" });\n\n\t\tlet user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));\n\t\tif (auth) {\n\t\t\tuser = await authModel.query().insert({\n\t\t\t\tuser_id: user.id,\n\t\t\t\ttype: auth.type,\n\t\t\t\tsecret: auth.secret,\n\t\t\t\tmeta: {},\n\t\t\t});\n\t\t}\n\n\t\t// Create permissions row as well\n\t\tconst isAdmin = data.roles.indexOf(\"admin\") !== -1;\n\n\t\tawait userPermissionModel.query().insert({\n\t\t\tuser_id: user.id,\n\t\t\tvisibility: isAdmin ? \"all\" : \"user\",\n\t\t\tproxy_hosts: \"manage\",\n\t\t\tredirection_hosts: \"manage\",\n\t\t\tdead_hosts: \"manage\",\n\t\t\tstreams: \"manage\",\n\t\t\taccess_lists: \"manage\",\n\t\t\tcertificates: \"manage\",\n\t\t});\n\n\t\tuser = await internalUser.get(access, { id: user.id, expand: [\"permissions\"] });\n\n\t\tawait internalAuditLog.add(access, {\n\t\t\taction: \"created\",\n\t\t\tobject_type: \"user\",\n\t\t\tobject_id: user.id,\n\t\t\tmeta: user,\n\t\t});\n\n\t\treturn user;\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Integer} data.id\n\t * @param  {String}  [data.email]\n\t * @param  {String}  [data.name]\n\t * @return {Promise}\n\t */\n\tupdate: (access, data) => {\n\t\tif (typeof data.is_disabled !== \"undefined\") {\n\t\t\tdata.is_disabled = data.is_disabled ? 1 : 0;\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"users:update\", data.id)\n\t\t\t.then(() => {\n\t\t\t\t// Make sure that the user being updated doesn't change their email to another user that is already using it\n\t\t\t\t// 1. get user we want to update\n\t\t\t\treturn internalUser.get(access, { id: data.id }).then((user) => {\n\t\t\t\t\t// 2. if email is to be changed, find other users with that email\n\t\t\t\t\tif (typeof data.email !== \"undefined\") {\n\t\t\t\t\t\tdata.email = data.email.toLowerCase().trim();\n\n\t\t\t\t\t\tif (user.email !== data.email) {\n\t\t\t\t\t\t\treturn internalUser.isEmailAvailable(data.email, data.id).then((available) => {\n\t\t\t\t\t\t\t\tif (!available) {\n\t\t\t\t\t\t\t\t\tthrow new errs.ValidationError(`Email address already in use - ${data.email}`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn user;\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// No change to email:\n\t\t\t\t\treturn user;\n\t\t\t\t});\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\tif (user.id !== data.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tdata.avatar = gravatar.url(data.email || user.email, { default: \"mm\" });\n\t\t\t\treturn userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn internalUser.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\t// Add to audit log\n\t\t\t\treturn internalAuditLog\n\t\t\t\t\t.add(access, {\n\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\tobject_type: \"user\",\n\t\t\t\t\t\tobject_id: user.id,\n\t\t\t\t\t\tmeta: { ...data, id: user.id, name: user.name },\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\treturn user;\n\t\t\t\t\t});\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}   access\n\t * @param  {Object}   [data]\n\t * @param  {Integer}  [data.id]          Defaults to the token user\n\t * @param  {Array}    [data.expand]\n\t * @param  {Array}    [data.omit]\n\t * @return {Promise}\n\t */\n\tget: (access, data) => {\n\t\tconst thisData = data || {};\n\n\t\tif (typeof thisData.id === \"undefined\" || !thisData.id) {\n\t\t\tthisData.id = access.token.getUserId(0);\n\t\t}\n\n\t\treturn access\n\t\t\t.can(\"users:get\", thisData.id)\n\t\t\t.then(() => {\n\t\t\t\tconst query = userModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"is_deleted\", 0)\n\t\t\t\t\t.andWhere(\"id\", thisData.id)\n\t\t\t\t\t.allowGraph(\"[permissions]\")\n\t\t\t\t\t.first();\n\n\t\t\t\tif (typeof thisData.expand !== \"undefined\" && thisData.expand !== null) {\n\t\t\t\t\tquery.withGraphFetched(`[${thisData.expand.join(\", \")}]`);\n\t\t\t\t}\n\n\t\t\t\treturn query.then(utils.omitRow(omissions()));\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\tif (!row || !row.id) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(thisData.id);\n\t\t\t\t}\n\t\t\t\t// Custom omissions\n\t\t\t\tif (typeof thisData.omit !== \"undefined\" && thisData.omit !== null) {\n\t\t\t\t\treturn _.omit(row, thisData.omit);\n\t\t\t\t}\n\n\t\t\t\tif (row.avatar === \"\") {\n\t\t\t\t\trow.avatar = DEFAULT_AVATAR;\n\t\t\t\t}\n\n\t\t\t\treturn row;\n\t\t\t});\n\t},\n\n\t/**\n\t * Checks if an email address is available, but if a user_id is supplied, it will ignore checking\n\t * against that user.\n\t *\n\t * @param email\n\t * @param user_id\n\t */\n\tisEmailAvailable: (email, user_id) => {\n\t\tconst query = userModel.query().where(\"email\", \"=\", email.toLowerCase().trim()).where(\"is_deleted\", 0).first();\n\n\t\tif (typeof user_id !== \"undefined\") {\n\t\t\tquery.where(\"id\", \"!=\", user_id);\n\t\t}\n\n\t\treturn query.then((user) => {\n\t\t\treturn !user;\n\t\t});\n\t},\n\n\t/**\n\t * @param {Access}  access\n\t * @param {Object}  data\n\t * @param {Integer} data.id\n\t * @param {String}  [data.reason]\n\t * @returns {Promise}\n\t */\n\tdelete: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"users:delete\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalUser.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\tif (!user) {\n\t\t\t\t\tthrow new errs.ItemNotFoundError(data.id);\n\t\t\t\t}\n\n\t\t\t\t// Make sure user can't delete themselves\n\t\t\t\tif (user.id === access.token.getUserId(0)) {\n\t\t\t\t\tthrow new errs.PermissionError(\"You cannot delete yourself.\");\n\t\t\t\t}\n\n\t\t\t\treturn userModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"id\", user.id)\n\t\t\t\t\t.patch({\n\t\t\t\t\t\tis_deleted: 1,\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to audit log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"deleted\",\n\t\t\t\t\t\t\tobject_type: \"user\",\n\t\t\t\t\t\t\tobject_id: user.id,\n\t\t\t\t\t\t\tmeta: _.omit(user, omissions()),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\tdeleteAll: async () => {\n\t\tawait userModel\n\t\t\t.query()\n\t\t\t.patch({\n\t\t\t\tis_deleted: 1,\n\t\t\t});\n\t},\n\n\t/**\n\t * This will only count the users\n\t *\n\t * @param   {Access}  access\n\t * @param   {String}  [search_query]\n\t * @returns {*}\n\t */\n\tgetCount: (access, search_query) => {\n\t\treturn access\n\t\t\t.can(\"users:list\")\n\t\t\t.then(() => {\n\t\t\t\tconst query = userModel.query().count(\"id as count\").where(\"is_deleted\", 0).first();\n\n\t\t\t\t// Query is used for searching\n\t\t\t\tif (typeof search_query === \"string\") {\n\t\t\t\t\tquery.where(function () {\n\t\t\t\t\t\tthis.where(\"user.name\", \"like\", `%${search_query}%`).orWhere(\n\t\t\t\t\t\t\t\"user.email\",\n\t\t\t\t\t\t\t\"like\",\n\t\t\t\t\t\t\t`%${search_query}%`,\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn query;\n\t\t\t})\n\t\t\t.then((row) => {\n\t\t\t\treturn Number.parseInt(row.count, 10);\n\t\t\t});\n\t},\n\n\t/**\n\t * All users\n\t *\n\t * @param   {Access}  access\n\t * @param   {Array}   [expand]\n\t * @param   {String}  [search_query]\n\t * @returns {Promise}\n\t */\n\tgetAll: async (access, expand, search_query) => {\n\t\tawait access.can(\"users:list\");\n\t\tconst query = userModel\n\t\t\t.query()\n\t\t\t.where(\"is_deleted\", 0)\n\t\t\t.groupBy(\"id\")\n\t\t\t.allowGraph(\"[permissions]\")\n\t\t\t.orderBy(\"name\", \"ASC\");\n\n\t\t// Query is used for searching\n\t\tif (typeof search_query === \"string\") {\n\t\t\tquery.where(function () {\n\t\t\t\tthis.where(\"name\", \"like\", `%${search_query}%`).orWhere(\"email\", \"like\", `%${search_query}%`);\n\t\t\t});\n\t\t}\n\n\t\tif (typeof expand !== \"undefined\" && expand !== null) {\n\t\t\tquery.withGraphFetched(`[${expand.join(\", \")}]`);\n\t\t}\n\n\t\tconst res = await query;\n\t\treturn utils.omitRows(omissions())(res);\n\t},\n\n\t/**\n\t * @param   {Access} access\n\t * @param   {Integer} [id_requested]\n\t * @returns {[String]}\n\t */\n\tgetUserOmisionsByAccess: (access, idRequested) => {\n\t\tlet response = []; // Admin response\n\n\t\tif (!access.token.hasScope(\"admin\") && access.token.getUserId(0) !== idRequested) {\n\t\t\tresponse = [\"is_deleted\"]; // Restricted response\n\t\t}\n\n\t\treturn response;\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @param  {Integer} data.id\n\t * @param  {String}  data.type\n\t * @param  {String}  data.secret\n\t * @return {Promise}\n\t */\n\tsetPassword: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"users:password\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalUser.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\tif (user.id !== data.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (user.id === access.token.getUserId(0)) {\n\t\t\t\t\t// they're setting their own password. Make sure their current password is correct\n\t\t\t\t\tif (typeof data.current === \"undefined\" || !data.current) {\n\t\t\t\t\t\tthrow new errs.ValidationError(\"Current password was not supplied\");\n\t\t\t\t\t}\n\n\t\t\t\t\treturn internalToken\n\t\t\t\t\t\t.getTokenFromEmail({\n\t\t\t\t\t\t\tidentity: user.email,\n\t\t\t\t\t\t\tsecret: data.current,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\treturn user;\n\t\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn user;\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\t// Get auth, patch if it exists\n\t\t\t\treturn authModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"user_id\", user.id)\n\t\t\t\t\t.andWhere(\"type\", data.type)\n\t\t\t\t\t.first()\n\t\t\t\t\t.then((existing_auth) => {\n\t\t\t\t\t\tif (existing_auth) {\n\t\t\t\t\t\t\t// patch\n\t\t\t\t\t\t\treturn authModel.query().where(\"user_id\", user.id).andWhere(\"type\", data.type).patch({\n\t\t\t\t\t\t\t\ttype: data.type, // This is required for the model to encrypt on save\n\t\t\t\t\t\t\t\tsecret: data.secret,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// insert\n\t\t\t\t\t\treturn authModel.query().insert({\n\t\t\t\t\t\t\tuser_id: user.id,\n\t\t\t\t\t\t\ttype: data.type,\n\t\t\t\t\t\t\tsecret: data.secret,\n\t\t\t\t\t\t\tmeta: {},\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t// Add to Audit Log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\t\tobject_type: \"user\",\n\t\t\t\t\t\t\tobject_id: user.id,\n\t\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\t\tname: user.name,\n\t\t\t\t\t\t\t\tpassword_changed: true,\n\t\t\t\t\t\t\t\tauth_type: data.type,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param  {Access}  access\n\t * @param  {Object}  data\n\t * @return {Promise}\n\t */\n\tsetPermissions: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"users:permissions\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalUser.get(access, { id: data.id });\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\tif (user.id !== data.id) {\n\t\t\t\t\t// Sanity check that something crazy hasn't happened\n\t\t\t\t\tthrow new errs.InternalValidationError(\n\t\t\t\t\t\t`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn user;\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\t// Get perms row, patch if it exists\n\t\t\t\treturn userPermissionModel\n\t\t\t\t\t.query()\n\t\t\t\t\t.where(\"user_id\", user.id)\n\t\t\t\t\t.first()\n\t\t\t\t\t.then((existing_auth) => {\n\t\t\t\t\t\tif (existing_auth) {\n\t\t\t\t\t\t\t// patch\n\t\t\t\t\t\t\treturn userPermissionModel\n\t\t\t\t\t\t\t\t.query()\n\t\t\t\t\t\t\t\t.where(\"user_id\", user.id)\n\t\t\t\t\t\t\t\t.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// insert\n\t\t\t\t\t\treturn userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));\n\t\t\t\t\t})\n\t\t\t\t\t.then((permissions) => {\n\t\t\t\t\t\t// Add to Audit Log\n\t\t\t\t\t\treturn internalAuditLog.add(access, {\n\t\t\t\t\t\t\taction: \"updated\",\n\t\t\t\t\t\t\tobject_type: \"user\",\n\t\t\t\t\t\t\tobject_id: user.id,\n\t\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\t\tname: user.name,\n\t\t\t\t\t\t\t\tpermissions: permissions,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn true;\n\t\t\t});\n\t},\n\n\t/**\n\t * @param {Access}   access\n\t * @param {Object}   data\n\t * @param {Integer}  data.id\n\t */\n\tloginAs: (access, data) => {\n\t\treturn access\n\t\t\t.can(\"users:loginas\", data.id)\n\t\t\t.then(() => {\n\t\t\t\treturn internalUser.get(access, data);\n\t\t\t})\n\t\t\t.then((user) => {\n\t\t\t\treturn internalToken.getTokenFromUser(user);\n\t\t\t});\n\t},\n};\n\nexport default internalUser;\n"
  },
  {
    "path": "backend/knexfile.js",
    "content": "module.exports = {\n\tdevelopment: {\n\t\tclient:     'mysql2',\n\t\tmigrations: {\n\t\t\ttableName: 'migrations',\n\t\t\tstub:      'lib/migrate_template.js',\n\t\t\tdirectory: 'migrations'\n\t\t}\n\t},\n\n\tproduction: {\n\t\tclient:     'mysql2',\n\t\tmigrations: {\n\t\t\ttableName: 'migrations',\n\t\t\tstub:      'lib/migrate_template.js',\n\t\t\tdirectory: 'migrations'\n\t\t}\n\t}\n};\n"
  },
  {
    "path": "backend/lib/access/access_lists-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access_lists\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_access_lists\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/access_lists-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access_lists\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_access_lists\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/access_lists-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access_lists\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_access_lists\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/access_lists-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access_lists\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_access_lists\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/access_lists-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access_lists\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_access_lists\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/auditlog-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/certificates-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certificates\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_certificates\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/certificates-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certificates\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_certificates\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/certificates-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certificates\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_certificates\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/certificates-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certificates\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_certificates\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/certificates-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certificates\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_certificates\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/dead_hosts-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_dead_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/dead_hosts-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_dead_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/dead_hosts-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_dead_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/dead_hosts-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_dead_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/dead_hosts-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_dead_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/permissions.json",
    "content": "{\n\t\"$id\": \"perms\",\n\t\"definitions\": {\n\t\t\"view\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^(view|manage)$\"\n\t\t},\n\t\t\"manage\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^(manage)$\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/lib/access/proxy_hosts-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_proxy_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/proxy_hosts-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_proxy_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/proxy_hosts-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_proxy_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/proxy_hosts-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_proxy_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/proxy_hosts-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_proxy_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/redirection_hosts-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redirection_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_redirection_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/redirection_hosts-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redirection_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_redirection_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/redirection_hosts-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redirection_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_redirection_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/redirection_hosts-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redirection_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_redirection_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/redirection_hosts-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redirection_hosts\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_redirection_hosts\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/reports-hosts.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/user\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/roles.json",
    "content": "{\n\t\"$id\": \"roles\",\n\t\"definitions\": {\n\t\t\"admin\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"scope\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"scope\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^user$\"\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^admin$\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"user\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"scope\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"scope\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^user$\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/lib/access/settings-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/settings-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/settings-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/streams-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_streams\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_streams\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/streams-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_streams\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_streams\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/streams-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_streams\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_streams\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/streams-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_streams\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_streams\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/view\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/streams-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_streams\", \"roles\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"permission_streams\": {\n\t\t\t\t\t\"$ref\": \"perms#/definitions/manage\"\n\t\t\t\t},\n\t\t\t\t\"roles\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"enum\": [\"user\"]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-create.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-delete.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-get.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"data\": {\n\t\t\t\t\t\"$ref\": \"objects#/properties/users\"\n\t\t\t\t},\n\t\t\t\t\"scope\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^user$\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-list.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-loginas.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-password.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"data\": {\n\t\t\t\t\t\"$ref\": \"objects#/properties/users\"\n\t\t\t\t},\n\t\t\t\t\"scope\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^user$\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-permissions.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access/users-update.json",
    "content": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"data\": {\n\t\t\t\t\t\"$ref\": \"objects#/properties/users\"\n\t\t\t\t},\n\t\t\t\t\"scope\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"contains\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^user$\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "backend/lib/access.js",
    "content": "/**\n * Some Notes: This is a friggin complicated piece of code.\n *\n * \"scope\" in this file means \"where did this token come from and what is using it\", so 99% of the time\n * the \"scope\" is going to be \"user\" because it would be a user token. This is not to be confused with\n * the \"role\" which could be \"user\" or \"admin\". The scope in fact, could be \"worker\" or anything else.\n */\n\nimport fs from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport Ajv from \"ajv/dist/2020.js\";\nimport _ from \"lodash\";\nimport { access as logger } from \"../logger.js\";\nimport proxyHostModel from \"../models/proxy_host.js\";\nimport TokenModel from \"../models/token.js\";\nimport userModel from \"../models/user.js\";\nimport permsSchema from \"./access/permissions.json\" with { type: \"json\" };\nimport roleSchema from \"./access/roles.json\" with { type: \"json\" };\nimport errs from \"./error.js\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nexport default function (tokenString) {\n\tconst Token = TokenModel();\n\tlet tokenData = null;\n\tlet initialised = false;\n\tconst objectCache = {};\n\tlet allowInternalAccess = false;\n\tlet userRoles = [];\n\tlet permissions = {};\n\n\t/**\n\t * Loads the Token object from the token string\n\t *\n\t * @returns {Promise}\n\t */\n\tthis.init = async () => {\n\t\tif (initialised) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!tokenString) {\n\t\t\tthrow new errs.PermissionError(\"Permission Denied\");\n\t\t}\n\n\t\ttokenData = await Token.load(tokenString);\n\n\t\t// At this point we need to load the user from the DB and make sure they:\n\t\t// - exist (and not soft deleted)\n\t\t// - still have the appropriate scopes for this token\n\t\t// This is only required when the User ID is supplied or if the token scope has `user`\n\t\tif (\n\t\t\ttokenData.attrs.id ||\n\t\t\t(typeof tokenData.scope !== \"undefined\" && _.indexOf(tokenData.scope, \"user\") !== -1)\n\t\t) {\n\t\t\t// Has token user id or token user scope\n\t\t\tconst user = await userModel\n\t\t\t\t.query()\n\t\t\t\t.where(\"id\", tokenData.attrs.id)\n\t\t\t\t.andWhere(\"is_deleted\", 0)\n\t\t\t\t.andWhere(\"is_disabled\", 0)\n\t\t\t\t.allowGraph(\"[permissions]\")\n\t\t\t\t.withGraphFetched(\"[permissions]\")\n\t\t\t\t.first();\n\n\t\t\tif (user) {\n\t\t\t\t// make sure user has all scopes of the token\n\t\t\t\t// The `user` role is not added against the user row, so we have to just add it here to get past this check.\n\t\t\t\tuser.roles.push(\"user\");\n\n\t\t\t\tlet ok = true;\n\t\t\t\t_.forEach(tokenData.scope, (scope_item) => {\n\t\t\t\t\tif (_.indexOf(user.roles, scope_item) === -1) {\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tif (!ok) {\n\t\t\t\t\tthrow new errs.AuthError(\"Invalid token scope for User\");\n\t\t\t\t}\n\t\t\t\tinitialised = true;\n\t\t\t\tuserRoles = user.roles;\n\t\t\t\tpermissions = user.permissions;\n\t\t\t} else {\n\t\t\t\tthrow new errs.AuthError(\"User cannot be loaded for Token\");\n\t\t\t}\n\t\t}\n\t\tinitialised = true;\n\t};\n\n\t/**\n\t * Fetches the object ids from the database, only once per object type, for this token.\n\t * This only applies to USER token scopes, as all other tokens are not really bound\n\t * by object scopes\n\t *\n\t * @param   {String} objectType\n\t * @returns {Promise}\n\t */\n\tthis.loadObjects = async (objectType) => {\n\t\tlet objects = null;\n\n\t\tif (Token.hasScope(\"user\")) {\n\t\t\tif (typeof tokenData.attrs.id === \"undefined\" || !tokenData.attrs.id) {\n\t\t\t\tthrow new errs.AuthError(\"User Token supplied without a User ID\");\n\t\t\t}\n\n\t\t\tconst tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0;\n\n\t\t\tif (typeof objectCache[objectType] !== \"undefined\") {\n\t\t\t\tobjects = objectCache[objectType];\n\t\t\t} else {\n\t\t\t\tswitch (objectType) {\n\t\t\t\t\t// USERS - should only return yourself\n\t\t\t\t\tcase \"users\":\n\t\t\t\t\t\tobjects = tokenUserId ? [tokenUserId] : [];\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t// Proxy Hosts\n\t\t\t\t\tcase \"proxy_hosts\": {\n\t\t\t\t\t\tconst query = proxyHostModel\n\t\t\t\t\t\t\t.query()\n\t\t\t\t\t\t\t.select(\"id\")\n\t\t\t\t\t\t\t.andWhere(\"is_deleted\", 0);\n\n\t\t\t\t\t\tif (permissions.visibility === \"user\") {\n\t\t\t\t\t\t\tquery.andWhere(\"owner_user_id\", tokenUserId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rows = await query;\n\t\t\t\t\t\tobjects = [];\n\t\t\t\t\t\t_.forEach(rows, (ruleRow) => {\n\t\t\t\t\t\t\tobjects.push(ruleRow.id);\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// enum should not have less than 1 item\n\t\t\t\t\t\tif (!objects.length) {\n\t\t\t\t\t\t\tobjects.push(0);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tobjectCache[objectType] = objects;\n\t\t\t}\n\t\t}\n\t\treturn objects;\n\t};\n\n\t/**\n\t * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema\n\t *\n\t * @param   {String} permissionLabel\n\t * @returns {Object}\n\t */\n\tthis.getObjectSchema = async (permissionLabel) => {\n\t\tconst baseObjectType = permissionLabel.split(\":\").shift();\n\n\t\tconst schema = {\n\t\t\t$id: \"objects\",\n\t\t\tdescription: \"Actor Properties\",\n\t\t\ttype: \"object\",\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tuser_id: {\n\t\t\t\t\tanyOf: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"number\",\n\t\t\t\t\t\t\tenum: [Token.get(\"attrs\").id],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\tscope: {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tpattern: `^${Token.get(\"scope\")}$`,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\n\t\tconst result = await this.loadObjects(baseObjectType);\n\t\tif (typeof result === \"object\" && result !== null) {\n\t\t\tschema.properties[baseObjectType] = {\n\t\t\t\ttype: \"number\",\n\t\t\t\tenum: result,\n\t\t\t\tminimum: 1,\n\t\t\t};\n\t\t} else {\n\t\t\tschema.properties[baseObjectType] = {\n\t\t\t\ttype: \"number\",\n\t\t\t\tminimum: 1,\n\t\t\t};\n\t\t}\n\n\t\treturn schema;\n\t};\n\n\t// here:\n\n\treturn {\n\t\ttoken: Token,\n\n\t\t/**\n\t\t *\n\t\t * @param   {Boolean}  [allowInternal]\n\t\t * @returns {Promise}\n\t\t */\n\t\tload: async (allowInternal) => {\n\t\t\tif (tokenString) {\n\t\t\t\treturn await Token.load(tokenString);\n\t\t\t}\n\t\t\tallowInternalAccess = allowInternal;\n\t\t\treturn allowInternal || null;\n\t\t},\n\n\t\treloadObjects: this.loadObjects,\n\n\t\t/**\n\t\t *\n\t\t * @param {String}  permission\n\t\t * @param {*}       [data]\n\t\t * @returns {Promise}\n\t\t */\n\t\tcan: async (permission, data) => {\n\t\t\tif (allowInternalAccess === true) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait this.init();\n\t\t\t\tconst objectSchema = await this.getObjectSchema(permission);\n\n\t\t\t\tconst dataSchema = {\n\t\t\t\t\t[permission]: {\n\t\t\t\t\t\tdata: data,\n\t\t\t\t\t\tscope: Token.get(\"scope\"),\n\t\t\t\t\t\troles: userRoles,\n\t\t\t\t\t\tpermission_visibility: permissions.visibility,\n\t\t\t\t\t\tpermission_proxy_hosts: permissions.proxy_hosts,\n\t\t\t\t\t\tpermission_redirection_hosts: permissions.redirection_hosts,\n\t\t\t\t\t\tpermission_dead_hosts: permissions.dead_hosts,\n\t\t\t\t\t\tpermission_streams: permissions.streams,\n\t\t\t\t\t\tpermission_access_lists: permissions.access_lists,\n\t\t\t\t\t\tpermission_certificates: permissions.certificates,\n\t\t\t\t\t},\n\t\t\t\t};\n\n\t\t\t\tconst permissionSchema = {\n\t\t\t\t\t$async: true,\n\t\t\t\t\t$id: \"permissions\",\n\t\t\t\t\ttype: \"object\",\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {},\n\t\t\t\t};\n\n\t\t\t\tconst rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, \"-\")}.json`, {\n\t\t\t\t\tencoding: \"utf8\",\n\t\t\t\t});\n\t\t\t\tpermissionSchema.properties[permission] = JSON.parse(rawData);\n\n\t\t\t\tconst ajv = new Ajv({\n\t\t\t\t\tverbose: true,\n\t\t\t\t\tallErrors: true,\n\t\t\t\t\tbreakOnError: true,\n\t\t\t\t\tcoerceTypes: true,\n\t\t\t\t\tschemas: [roleSchema, permsSchema, objectSchema, permissionSchema],\n\t\t\t\t});\n\n\t\t\t\tconst valid = await ajv.validate(\"permissions\", dataSchema);\n\t\t\t\treturn valid && dataSchema[permission];\n\t\t\t} catch (err) {\n\t\t\t\terr.permission = permission;\n\t\t\t\terr.permission_data = data;\n\t\t\t\tlogger.error(permission, data, err.message);\n\t\t\t\tthrow errs.PermissionError(\"Permission Denied\", err);\n\t\t\t}\n\t\t},\n\t};\n}\n"
  },
  {
    "path": "backend/lib/certbot.js",
    "content": "import batchflow from \"batchflow\";\nimport dnsPlugins from \"../certbot/dns-plugins.json\" with { type: \"json\" };\nimport { certbot as logger } from \"../logger.js\";\nimport errs from \"./error.js\";\nimport utils from \"./utils.js\";\n\nconst CERTBOT_VERSION_REPLACEMENT = \"$(certbot --version | grep -Eo '[0-9](\\\\.[0-9]+)+')\";\n\n/**\n * Installs a cerbot plugin given the key for the object from\n * ../certbot/dns-plugins.json\n *\n * @param   {string}  pluginKey\n * @returns {Object}\n */\nconst installPlugin = async (pluginKey) => {\n\tif (typeof dnsPlugins[pluginKey] === \"undefined\") {\n\t\t// throw Error(`Certbot plugin ${pluginKey} not found`);\n\t\tthrow new errs.ItemNotFoundError(pluginKey);\n\t}\n\n\tconst plugin = dnsPlugins[pluginKey];\n\tlogger.start(`Installing ${pluginKey}...`);\n\n\tplugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);\n\tplugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);\n\n\t// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly\n\t// in new versions of Python\n\tlet env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: \"stdlib\" });\n\tif (typeof plugin.env === \"object\") {\n\t\tenv = Object.assign(env, plugin.env);\n\t}\n\n\tconst cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version}  && deactivate`;\n\treturn utils\n\t\t.exec(cmd, { env })\n\t\t.then((result) => {\n\t\t\tlogger.complete(`Installed ${pluginKey}`);\n\t\t\treturn result;\n\t\t})\n\t\t.catch((err) => {\n\t\t\tthrow err;\n\t\t});\n};\n\n/**\n * @param {array} pluginKeys\n */\nconst installPlugins = async (pluginKeys) => {\n\tlet hasErrors = false;\n\n\treturn new Promise((resolve, reject) => {\n\t\tif (pluginKeys.length === 0) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\n\t\tbatchflow(pluginKeys)\n\t\t\t.sequential()\n\t\t\t.each((_i, pluginKey, next) => {\n\t\t\t\tinstallPlugin(pluginKey)\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\tnext();\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\thasErrors = true;\n\t\t\t\t\t\tnext(err);\n\t\t\t\t\t});\n\t\t\t})\n\t\t\t.error((err) => {\n\t\t\t\tlogger.error(err.message);\n\t\t\t})\n\t\t\t.end(() => {\n\t\t\t\tif (hasErrors) {\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew errs.CommandError(\"Some plugins failed to install. Please check the logs above\", 1),\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t});\n};\n\nexport { installPlugins, installPlugin };\n"
  },
  {
    "path": "backend/lib/config.js",
    "content": "import fs from \"node:fs\";\nimport NodeRSA from \"node-rsa\";\nimport { global as logger } from \"../logger.js\";\n\nconst keysFile               = '/data/keys.json';\nconst mysqlEngine            = 'mysql2';\nconst postgresEngine         = 'pg';\nconst sqliteClientName       = 'better-sqlite3';\n\n// Not used for new setups anymore but may exist in legacy setups\nconst legacySqliteClientName = 'sqlite3';\n\nlet instance = null;\n\n// 1. Load from config file first (not recommended anymore)\n// 2. Use config env variables next\nconst configure = () => {\n\tconst filename = `${process.env.NODE_CONFIG_DIR || \"./config\"}/${process.env.NODE_ENV || \"default\"}.json`;\n\tif (fs.existsSync(filename)) {\n\t\tlet configData;\n\t\ttry {\n\t\t\t// Load this json  synchronously\n\t\t\tconst rawData = fs.readFileSync(filename);\n\t\t\tconfigData = JSON.parse(rawData);\n\t\t} catch (_) {\n\t\t\t// do nothing\n\t\t}\n\n\t\tif (configData?.database) {\n\t\t\tlogger.info(`Using configuration from file: ${filename}`);\n\n\t\t\t// Migrate those who have \"mysql\" engine to \"mysql2\"\n\t\t\tif (configData.database.engine === \"mysql\") {\n\t\t\t\tconfigData.database.engine = mysqlEngine;\n\t\t\t}\n\n\t\t\tinstance = configData;\n\t\t\tinstance.keys = getKeys();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim());\n\n\tconst envMysqlHost                  = process.env.DB_MYSQL_HOST || null;\n\tconst envMysqlUser                  = process.env.DB_MYSQL_USER || null;\n\tconst envMysqlName                  = process.env.DB_MYSQL_NAME || null;\n\tconst envMysqlSSL                   = toBool(process.env.DB_MYSQL_SSL);\n\tconst envMysqlSSLRejectUnauthorized\t= process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);\n\tconst envMysqlSSLVerifyIdentity\t\t= process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);\n\tif (envMysqlHost && envMysqlUser && envMysqlName) {\n\t\t// we have enough mysql creds to go with mysql\n\t\tlogger.info(\"Using MySQL configuration\");\n\t\tinstance = {\n\t\t\tdatabase: {\n\t\t\t\tengine: mysqlEngine,\n\t\t\t\thost: envMysqlHost,\n\t\t\t\tport: process.env.DB_MYSQL_PORT || 3306,\n\t\t\t\tuser: envMysqlUser,\n\t\t\t\tpassword: process.env.DB_MYSQL_PASSWORD,\n\t\t\t\tname:     envMysqlName,\n\t\t\t\tssl:      envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,\n\t\t\t},\n\t\t\tkeys: getKeys(),\n\t\t};\n\t\treturn;\n\t}\n\n\tconst envPostgresHost = process.env.DB_POSTGRES_HOST || null;\n\tconst envPostgresUser = process.env.DB_POSTGRES_USER || null;\n\tconst envPostgresName = process.env.DB_POSTGRES_NAME || null;\n\tif (envPostgresHost && envPostgresUser && envPostgresName) {\n\t\t// we have enough postgres creds to go with postgres\n\t\tlogger.info(\"Using Postgres configuration\");\n\t\tinstance = {\n\t\t\tdatabase: {\n\t\t\t\tengine: postgresEngine,\n\t\t\t\thost: envPostgresHost,\n\t\t\t\tport: process.env.DB_POSTGRES_PORT || 5432,\n\t\t\t\tuser: envPostgresUser,\n\t\t\t\tpassword: process.env.DB_POSTGRES_PASSWORD,\n\t\t\t\tname: envPostgresName,\n\t\t\t},\n\t\t\tkeys: getKeys(),\n\t\t};\n\t\treturn;\n\t}\n\n\tconst envSqliteFile = process.env.DB_SQLITE_FILE || \"/data/database.sqlite\";\n\n\tlogger.info(`Using Sqlite: ${envSqliteFile}`);\n\tinstance = {\n\t\tdatabase: {\n\t\t\tengine: \"knex-native\",\n\t\t\tknex: {\n\t\t\t\tclient: sqliteClientName,\n\t\t\t\tconnection: {\n\t\t\t\t\tfilename: envSqliteFile,\n\t\t\t\t},\n\t\t\t\tuseNullAsDefault: true,\n\t\t\t},\n\t\t},\n\t\tkeys: getKeys(),\n\t};\n};\n\nconst getKeys = () => {\n\t// Get keys from file\n\tif (isDebugMode()) {\n\t\tlogger.debug(\"Checking for keys file:\", keysFile);\n\t}\n\tif (!fs.existsSync(keysFile)) {\n\t\tgenerateKeys();\n\t} else if (process.env.DEBUG) {\n\t\tlogger.info(\"Keys file exists OK\");\n\t}\n\ttry {\n\t\t// Load this json keysFile synchronously and return the json object\n\t\tconst rawData = fs.readFileSync(keysFile);\n\t\treturn JSON.parse(rawData);\n\t} catch (err) {\n\t\tlogger.error(`Could not read JWT key pair from config file: ${keysFile}`, err);\n\t\tprocess.exit(1);\n\t}\n};\n\nconst generateKeys = () => {\n\tlogger.info(\"Creating a new JWT key pair...\");\n\t// Now create the keys and save them in the config.\n\tconst key = new NodeRSA({ b: 2048 });\n\tkey.generateKeyPair();\n\n\tconst keys = {\n\t\tkey: key.exportKey(\"private\").toString(),\n\t\tpub: key.exportKey(\"public\").toString(),\n\t};\n\n\t// Write keys config\n\ttry {\n\t\tfs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));\n\t} catch (err) {\n\t\tlogger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`);\n\t\tprocess.exit(1);\n\t}\n\tlogger.info(`Wrote JWT key pair to config file: ${keysFile}`);\n};\n\n/**\n *\n * @param   {string}  key   ie: 'database' or 'database.engine'\n * @returns {boolean}\n */\nconst configHas = (key) => {\n\tinstance === null && configure();\n\tconst keys = key.split(\".\");\n\tlet level = instance;\n\tlet has = true;\n\tkeys.forEach((keyItem) => {\n\t\tif (typeof level[keyItem] === \"undefined\") {\n\t\t\thas = false;\n\t\t} else {\n\t\t\tlevel = level[keyItem];\n\t\t}\n\t});\n\n\treturn has;\n};\n\n/**\n * Gets a specific key from the top level\n *\n * @param {string} key\n * @returns {*}\n */\nconst configGet = (key) => {\n\tinstance === null && configure();\n\tif (key && typeof instance[key] !== \"undefined\") {\n\t\treturn instance[key];\n\t}\n\treturn instance;\n};\n\n/**\n * Is this a sqlite configuration?\n *\n * @returns {boolean}\n */\nconst isSqlite = () => {\n\tinstance === null && configure();\n\treturn instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);\n};\n\n/**\n * Is this a mysql configuration?\n *\n * @returns {boolean}\n */\nconst isMysql = () => {\n\tinstance === null && configure();\n\treturn instance.database.engine === mysqlEngine;\n};\n\n/**\n * Is this a postgres configuration?\n *\n * @returns {boolean}\n */\nconst isPostgres = () => {\n\tinstance === null && configure();\n\treturn instance.database.engine === postgresEngine;\n};\n\n/**\n * Are we running in debug mdoe?\n *\n * @returns {boolean}\n */\nconst isDebugMode = () => !!process.env.DEBUG;\n\n/**\n * Are we running in CI?\n *\n * @returns {boolean}\n */\nconst isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';\n\n/**\n * Returns a public key\n *\n * @returns {string}\n */\nconst getPublicKey = () => {\n\tinstance === null && configure();\n\treturn instance.keys.pub;\n};\n\n/**\n * Returns a private key\n *\n * @returns {string}\n */\nconst getPrivateKey = () => {\n\tinstance === null && configure();\n\treturn instance.keys.key;\n};\n\n/**\n * @returns {boolean}\n */\nconst useLetsencryptStaging = () => !!process.env.LE_STAGING;\n\n/**\n * @returns {string|null}\n */\nconst useLetsencryptServer = () => {\n\tif (process.env.LE_SERVER) {\n\t\treturn process.env.LE_SERVER;\n\t}\n\treturn null;\n};\n\nexport { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };\n"
  },
  {
    "path": "backend/lib/error.js",
    "content": "import _ from \"lodash\";\n\nconst errs = {\n\tPermissionError: function (_, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = \"Permission Denied\";\n\t\tthis.public = true;\n\t\tthis.status = 403;\n\t},\n\n\tItemNotFoundError: function (id, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = \"Not Found\";\n\t\tif (id) {\n\t\t\tthis.message = `Not Found - ${id}`;\n\t\t}\n\t\tthis.public = true;\n\t\tthis.status = 404;\n\t},\n\n\tAuthError: function (message, messageI18n, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.message_i18n = messageI18n;\n\t\tthis.public = true;\n\t\tthis.status = 400;\n\t},\n\n\tInternalError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.status = 500;\n\t\tthis.public = false;\n\t},\n\n\tInternalValidationError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.status = 400;\n\t\tthis.public = false;\n\t},\n\n\tConfigurationError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.status = 400;\n\t\tthis.public = true;\n\t},\n\n\tCacheError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.message = message;\n\t\tthis.previous = previous;\n\t\tthis.status = 500;\n\t\tthis.public = false;\n\t},\n\n\tValidationError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.public = true;\n\t\tthis.status = 400;\n\t},\n\n\tAssertionFailedError: function (message, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = message;\n\t\tthis.public = false;\n\t\tthis.status = 400;\n\t},\n\n\tCommandError: function (stdErr, code, previous) {\n\t\tError.captureStackTrace(this, this.constructor);\n\t\tthis.name = this.constructor.name;\n\t\tthis.previous = previous;\n\t\tthis.message = stdErr;\n\t\tthis.code = code;\n\t\tthis.public = false;\n\t},\n};\n\n_.forEach(errs, (err) => {\n\terr.prototype = Object.create(Error.prototype);\n});\n\nexport default errs;\n"
  },
  {
    "path": "backend/lib/express/cors.js",
    "content": "export default (req, res, next) => {\n\tif (req.headers.origin) {\n\t\tres.set({\n\t\t\t\"Access-Control-Allow-Origin\": req.headers.origin,\n\t\t\t\"Access-Control-Allow-Credentials\": true,\n\t\t\t\"Access-Control-Allow-Methods\": \"OPTIONS, GET, POST\",\n\t\t\t\"Access-Control-Allow-Headers\":\n\t\t\t\t\"Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit\",\n\t\t\t\"Access-Control-Max-Age\": 5 * 60,\n\t\t\t\"Access-Control-Expose-Headers\": \"X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit\",\n\t\t});\n\t\tnext();\n\t} else {\n\t\t// No origin\n\t\tnext();\n\t}\n};\n"
  },
  {
    "path": "backend/lib/express/jwt-decode.js",
    "content": "import Access from \"../access.js\";\n\nexport default () => {\n\treturn async (_, res, next) => {\n\t\ttry {\n\t\t\tres.locals.access = null;\n\t\t\tconst access = new Access(res.locals.token || null);\n\t\t\tawait access.load();\n\t\t\tres.locals.access = access;\n\t\t\tnext();\n\t\t} catch (err) {\n\t\t\tnext(err);\n\t\t}\n\t};\n};\n"
  },
  {
    "path": "backend/lib/express/jwt.js",
    "content": "export default function () {\n\treturn (req, res, next) => {\n\t\tif (req.headers.authorization) {\n\t\t\tconst parts = req.headers.authorization.split(\" \");\n\n\t\t\tif (parts && parts[0] === \"Bearer\" && parts[1]) {\n\t\t\t\tres.locals.token = parts[1];\n\t\t\t}\n\t\t}\n\n\t\tnext();\n\t};\n}\n"
  },
  {
    "path": "backend/lib/express/pagination.js",
    "content": "import _  from \"lodash\";\n\nexport default (default_sort, default_offset, default_limit, max_limit) => {\n\t/**\n\t * This will setup the req query params with filtered data and defaults\n\t *\n\t * sort    will be an array of fields and their direction\n\t * offset  will be an int, defaulting to zero if no other default supplied\n\t * limit   will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied\n\t *\n\t */\n\n\treturn (req, _res, next) => {\n\t\treq.query.offset =\n\t\t\ttypeof req.query.limit === \"undefined\" ? default_offset || 0 : Number.parseInt(req.query.offset, 10);\n\t\treq.query.limit =\n\t\t\ttypeof req.query.limit === \"undefined\" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);\n\n\t\tif (max_limit && req.query.limit > max_limit) {\n\t\t\treq.query.limit = max_limit;\n\t\t}\n\n\t\t// Sorting\n\t\tlet sort = typeof req.query.sort === \"undefined\" ? default_sort : req.query.sort;\n\t\tconst myRegexp = /.*\\.(asc|desc)$/gi;\n\t\tconst sort_array = [];\n\n\t\tsort = sort.split(\",\");\n\t\t_.map(sort, (val) => {\n\t\t\tconst matches = myRegexp.exec(val);\n\n\t\t\tif (matches !== null) {\n\t\t\t\tconst dir = matches[1];\n\t\t\t\tsort_array.push({\n\t\t\t\t\tfield: val.substr(0, val.length - (dir.length + 1)),\n\t\t\t\t\tdir: dir.toLowerCase(),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tsort_array.push({\n\t\t\t\t\tfield: val,\n\t\t\t\t\tdir: \"asc\",\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\t// Sort will now be in this format:\n\t\t// [\n\t\t//    { field: 'field1', dir: 'asc' },\n\t\t//    { field: 'field2', dir: 'desc' }\n\t\t// ]\n\n\t\treq.query.sort = sort_array;\n\t\tnext();\n\t};\n};\n"
  },
  {
    "path": "backend/lib/express/user-id-from-me.js",
    "content": "export default (req, res, next) => {\n\tif (req.params.user_id === 'me' && res.locals.access) {\n\t\treq.params.user_id = res.locals.access.token.get('attrs').id;\n\t} else {\n\t\treq.params.user_id = Number.parseInt(req.params.user_id, 10);\n\t}\n\tnext();\n};\n"
  },
  {
    "path": "backend/lib/helpers.js",
    "content": "import moment from \"moment\";\nimport { ref } from \"objection\";\nimport { isPostgres } from \"./config.js\";\n\n/**\n * Takes an expression such as 30d and returns a moment object of that date in future\n *\n * Key      Shorthand\n * ==================\n * years         y\n * quarters      Q\n * months        M\n * weeks         w\n * days          d\n * hours         h\n * minutes       m\n * seconds       s\n * milliseconds  ms\n *\n * @param {String}  expression\n * @returns {Object}\n */\nconst parseDatePeriod = (expression) => {\n\tconst matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);\n\tif (matches) {\n\t\treturn moment().add(matches[1], matches[2]);\n\t}\n\n\treturn null;\n};\n\nconst convertIntFieldsToBool = (obj, fields) => {\n\tfields.forEach((field) => {\n\t\tif (typeof obj[field] !== \"undefined\") {\n\t\t\tobj[field] = obj[field] === 1;\n\t\t}\n\t});\n\treturn obj;\n};\n\nconst convertBoolFieldsToInt = (obj, fields) => {\n\tfields.forEach((field) => {\n\t\tif (typeof obj[field] !== \"undefined\") {\n\t\t\tobj[field] = obj[field] ? 1 : 0;\n\t\t}\n\t});\n\treturn obj;\n};\n\n/**\n * Casts a column to json if using postgres\n *\n * @param {string} colName\n * @returns {string|Objection.ReferenceBuilder}\n */\nconst castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName);\n\nexport { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed };\n"
  },
  {
    "path": "backend/lib/migrate_template.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"identifier_for_migrate\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (_knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\t// Create Table example:\n\n\t/*\n\treturn knex.schema.createTable('notification', (table) => {\n\t\t table.increments().primary();\n\t\t table.string('name').notNull();\n\t\t table.string('type').notNull();\n\t\t table.integer('created_on').notNull();\n\t\t table.integer('modified_on').notNull();\n\t })\n\t\t.then(function () {\n\t\t\tlogger.info('[' + migrateName + '] Notification Table created');\n\t\t});\n\t */\n\n\tlogger.info(`[${migrateName}] Migrating Up Complete`);\n\n\treturn Promise.resolve(true);\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\t// Drop table example:\n\n\t/*\n\treturn knex.schema.dropTable('notification')\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] Notification Table dropped`);\n\t\t});\n\t*/\n\n\tlogger.info(`[${migrateName}] Migrating Down Complete`);\n\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/lib/utils.js",
    "content": "import { exec as nodeExec, execFile as nodeExecFile } from \"node:child_process\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Liquid } from \"liquidjs\";\nimport _ from \"lodash\";\nimport { debug, global as logger } from \"../logger.js\";\nimport errs from \"./error.js\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst exec = async (cmd, options = {}) => {\n\tdebug(logger, \"CMD:\", cmd);\n\tconst { stdout, stderr } = await new Promise((resolve, reject) => {\n\t\tconst child = nodeExec(cmd, options, (isError, stdout, stderr) => {\n\t\t\tif (isError) {\n\t\t\t\treject(new errs.CommandError(stderr, isError));\n\t\t\t} else {\n\t\t\t\tresolve({ stdout, stderr });\n\t\t\t}\n\t\t});\n\n\t\tchild.on(\"error\", (e) => {\n\t\t\treject(new errs.CommandError(stderr, 1, e));\n\t\t});\n\t});\n\treturn stdout;\n};\n\n/**\n * @param   {String} cmd\n * @param   {Array}  args\n * @param   {Object|undefined}  options\n * @returns {Promise}\n */\nconst execFile = (cmd, args, options) => {\n\tdebug(logger, `CMD: ${cmd} ${args ? args.join(\" \") : \"\"}`);\n\tconst opts = options || {};\n\n\treturn new Promise((resolve, reject) => {\n\t\tnodeExecFile(cmd, args, opts, (err, stdout, stderr) => {\n\t\t\tif (err && typeof err === \"object\") {\n\t\t\t\treject(new errs.CommandError(stderr, 1, err));\n\t\t\t} else {\n\t\t\t\tresolve(stdout.trim());\n\t\t\t}\n\t\t});\n\t});\n};\n\n/**\n * Used in objection query builder\n *\n * @param   {Array}  omissions\n * @returns {Function}\n */\nconst omitRow = (omissions) => {\n\t/**\n\t * @param   {Object} row\n\t * @returns {Object}\n\t */\n\treturn (row) => {\n\t\treturn _.omit(row, omissions);\n\t};\n};\n\n/**\n * Used in objection query builder\n *\n * @param   {Array}  omissions\n * @returns {Function}\n */\nconst omitRows = (omissions) => {\n\t/**\n\t * @param   {Array} rows\n\t * @returns {Object}\n\t */\n\treturn (rows) => {\n\t\trows.forEach((row, idx) => {\n\t\t\trows[idx] = _.omit(row, omissions);\n\t\t});\n\t\treturn rows;\n\t};\n};\n\n/**\n * @returns {Object} Liquid render engine\n */\nconst getRenderEngine = () => {\n\tconst renderEngine = new Liquid({\n\t\troot: `${__dirname}/../templates/`,\n\t});\n\n\t/**\n\t * nginxAccessRule expects the object given to have 2 properties:\n\t *\n\t * directive  string\n\t * address    string\n\t */\n\trenderEngine.registerFilter(\"nginxAccessRule\", (v) => {\n\t\tif (typeof v.directive !== \"undefined\" && typeof v.address !== \"undefined\" && v.directive && v.address) {\n\t\t\treturn `${v.directive} ${v.address};`;\n\t\t}\n\t\treturn \"\";\n\t});\n\n\treturn renderEngine;\n};\n\nexport default { exec, execFile, omitRow, omitRows, getRenderEngine };\n"
  },
  {
    "path": "backend/lib/validator/api.js",
    "content": "import Ajv from \"ajv/dist/2020.js\";\nimport errs from \"../error.js\";\n\nconst ajv = new Ajv({\n\tverbose: true,\n\tallErrors: true,\n\tallowUnionTypes: true,\n\tstrict: false,\n\tcoerceTypes: true,\n});\n\n/**\n * @param {Object} schema\n * @param {Object} payload\n * @returns {Promise}\n */\nconst apiValidator = async (schema, payload /*, description*/) => {\n\tif (!schema) {\n\t\tthrow new errs.ValidationError(\"Schema is undefined\");\n\t}\n\n\t// Can't use falsy check here as valid payload could be `0` or `false`\n\tif (typeof payload === \"undefined\") {\n\t\tthrow new errs.ValidationError(\"Payload is undefined\");\n\t}\n\n\n\tconst validate = ajv.compile(schema);\n\n\tconst valid = validate(payload);\n\n\n\tif (valid && !validate.errors) {\n\t\treturn payload;\n\t}\n\n\n\n\tconst message = ajv.errorsText(validate.errors);\n\tconst err = new errs.ValidationError(message);\n\terr.debug = {validationErrors: validate.errors, payload};\n\tthrow err;\n};\n\nexport default apiValidator;\n"
  },
  {
    "path": "backend/lib/validator/index.js",
    "content": "import Ajv from 'ajv/dist/2020.js';\nimport _ from \"lodash\";\nimport commonDefinitions from \"../../schema/common.json\" with { type: \"json\" };\nimport errs from \"../error.js\";\n\nRegExp.prototype.toJSON = RegExp.prototype.toString;\n\nconst ajv = new Ajv({\n\tverbose: true,\n\tallErrors: true,\n\tallowUnionTypes: true,\n\tcoerceTypes: true,\n\tstrict: false,\n\tschemas: [commonDefinitions],\n});\n\n/**\n *\n * @param   {Object} schema\n * @param   {Object} payload\n * @returns {Promise}\n */\nconst validator = (schema, payload) => {\n\treturn new Promise((resolve, reject) => {\n\t\tif (!payload) {\n\t\t\treject(new errs.InternalValidationError(\"Payload is falsy\"));\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tconst validate = ajv.compile(schema);\n\t\t\t\tconst valid = validate(payload);\n\n\t\t\t\tif (valid && !validate.errors) {\n\t\t\t\t\tresolve(_.cloneDeep(payload));\n\t\t\t\t} else {\n\t\t\t\t\tconst message = ajv.errorsText(validate.errors);\n\t\t\t\t\treject(new errs.InternalValidationError(message));\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\treject(err);\n\t\t\t}\n\t\t}\n\t});\n};\n\nexport default validator;\n"
  },
  {
    "path": "backend/logger.js",
    "content": "import signale from \"signale\";\nimport { isDebugMode } from \"./lib/config.js\";\n\nconst opts = {\n\tlogLevel: \"info\",\n};\n\nconst global = new signale.Signale({ scope: \"Global   \", ...opts });\nconst migrate = new signale.Signale({ scope: \"Migrate  \", ...opts });\nconst express = new signale.Signale({ scope: \"Express  \", ...opts });\nconst access = new signale.Signale({ scope: \"Access   \", ...opts });\nconst nginx = new signale.Signale({ scope: \"Nginx    \", ...opts });\nconst ssl = new signale.Signale({ scope: \"SSL      \", ...opts });\nconst certbot = new signale.Signale({ scope: \"Certbot  \", ...opts });\nconst importer = new signale.Signale({ scope: \"Importer \", ...opts });\nconst setup = new signale.Signale({ scope: \"Setup    \", ...opts });\nconst ipRanges = new signale.Signale({ scope: \"IP Ranges\", ...opts });\nconst remoteVersion = new signale.Signale({ scope: \"Remote Version\", ...opts });\n\nconst debug = (logger, ...args) => {\n\tif (isDebugMode()) {\n\t\tlogger.debug(...args);\n\t}\n};\n\nexport { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };\n"
  },
  {
    "path": "backend/migrate.js",
    "content": "import db from \"./db.js\";\nimport { migrate as logger } from \"./logger.js\";\n\nconst migrateUp = async () => {\n\tconst version = await db().migrate.currentVersion();\n\tlogger.info(\"Current database version:\", version);\n\treturn await db().migrate.latest({\n\t\ttableName: \"migrations\",\n\t\tdirectory: \"migrations\",\n\t});\n};\n\nexport { migrateUp };\n"
  },
  {
    "path": "backend/migrations/20180618015850_initial.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"initial-schema\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.createTable(\"auth\", (table) => {\n\t\t\ttable.increments().primary();\n\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\ttable.integer(\"user_id\").notNull().unsigned();\n\t\t\ttable.string(\"type\", 30).notNull();\n\t\t\ttable.string(\"secret\").notNull();\n\t\t\ttable.json(\"meta\").notNull();\n\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] auth Table created`);\n\n\t\t\treturn knex.schema.createTable(\"user\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"is_disabled\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.string(\"email\").notNull();\n\t\t\t\ttable.string(\"name\").notNull();\n\t\t\t\ttable.string(\"nickname\").notNull();\n\t\t\t\ttable.string(\"avatar\").notNull();\n\t\t\t\ttable.json(\"roles\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] user Table created`);\n\n\t\t\treturn knex.schema.createTable(\"user_permission\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"user_id\").notNull().unsigned();\n\t\t\t\ttable.string(\"visibility\").notNull();\n\t\t\t\ttable.string(\"proxy_hosts\").notNull();\n\t\t\t\ttable.string(\"redirection_hosts\").notNull();\n\t\t\t\ttable.string(\"dead_hosts\").notNull();\n\t\t\t\ttable.string(\"streams\").notNull();\n\t\t\t\ttable.string(\"access_lists\").notNull();\n\t\t\t\ttable.string(\"certificates\").notNull();\n\t\t\t\ttable.unique(\"user_id\");\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] user_permission Table created`);\n\n\t\t\treturn knex.schema.createTable(\"proxy_host\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.json(\"domain_names\").notNull();\n\t\t\t\ttable.string(\"forward_ip\").notNull();\n\t\t\t\ttable.integer(\"forward_port\").notNull().unsigned();\n\t\t\t\ttable.integer(\"access_list_id\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"certificate_id\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"ssl_forced\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"caching_enabled\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"block_exploits\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.text(\"advanced_config\").notNull().defaultTo(\"\");\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table created`);\n\n\t\t\treturn knex.schema.createTable(\"redirection_host\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.json(\"domain_names\").notNull();\n\t\t\t\ttable.string(\"forward_domain_name\").notNull();\n\t\t\t\ttable.integer(\"preserve_path\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"certificate_id\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"ssl_forced\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"block_exploits\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.text(\"advanced_config\").notNull().defaultTo(\"\");\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table created`);\n\n\t\t\treturn knex.schema.createTable(\"dead_host\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.json(\"domain_names\").notNull();\n\t\t\t\ttable.integer(\"certificate_id\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"ssl_forced\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.text(\"advanced_config\").notNull().defaultTo(\"\");\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] dead_host Table created`);\n\n\t\t\treturn knex.schema.createTable(\"stream\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"incoming_port\").notNull().unsigned();\n\t\t\t\ttable.string(\"forward_ip\").notNull();\n\t\t\t\ttable.integer(\"forwarding_port\").notNull().unsigned();\n\t\t\t\ttable.integer(\"tcp_forwarding\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.integer(\"udp_forwarding\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table created`);\n\n\t\t\treturn knex.schema.createTable(\"access_list\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.string(\"name\").notNull();\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list Table created`);\n\n\t\t\treturn knex.schema.createTable(\"certificate\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"owner_user_id\").notNull().unsigned();\n\t\t\t\ttable.integer(\"is_deleted\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.string(\"provider\").notNull();\n\t\t\t\ttable.string(\"nice_name\").notNull().defaultTo(\"\");\n\t\t\t\ttable.json(\"domain_names\").notNull();\n\t\t\t\ttable.dateTime(\"expires_on\").notNull();\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] certificate Table created`);\n\n\t\t\treturn knex.schema.createTable(\"access_list_auth\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"access_list_id\").notNull().unsigned();\n\t\t\t\ttable.string(\"username\").notNull();\n\t\t\t\ttable.string(\"password\").notNull();\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list_auth Table created`);\n\n\t\t\treturn knex.schema.createTable(\"audit_log\", (table) => {\n\t\t\t\ttable.increments().primary();\n\t\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\t\ttable.integer(\"user_id\").notNull().unsigned();\n\t\t\t\ttable.string(\"object_type\").notNull().defaultTo(\"\");\n\t\t\t\ttable.integer(\"object_id\").notNull().unsigned().defaultTo(0);\n\t\t\t\ttable.string(\"action\").notNull();\n\t\t\t\ttable.json(\"meta\").notNull();\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] audit_log Table created`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down the initial data.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20180929054513_websockets.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"websockets\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.integer(\"allow_websocket_upgrade\").notNull().unsigned().defaultTo(0);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20181019052346_forward_host.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"forward_host\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.renameColumn(\"forward_ip\", \"forward_host\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20181113041458_http2_support.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"http2_support\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.integer(\"http2_support\").notNull().unsigned().defaultTo(0);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"redirection_host\", (redirection_host) => {\n\t\t\t\tredirection_host.integer(\"http2_support\").notNull().unsigned().defaultTo(0);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"dead_host\", (dead_host) => {\n\t\t\t\tdead_host.integer(\"http2_support\").notNull().unsigned().defaultTo(0);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] dead_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20181213013211_forward_scheme.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"forward_scheme\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.string(\"forward_scheme\").notNull().defaultTo(\"http\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20190104035154_disabled.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"disabled\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.integer(\"enabled\").notNull().unsigned().defaultTo(1);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"redirection_host\", (redirection_host) => {\n\t\t\t\tredirection_host.integer(\"enabled\").notNull().unsigned().defaultTo(1);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"dead_host\", (dead_host) => {\n\t\t\t\tdead_host.integer(\"enabled\").notNull().unsigned().defaultTo(1);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] dead_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"stream\", (stream) => {\n\t\t\t\tstream.integer(\"enabled\").notNull().unsigned().defaultTo(1);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20190215115310_customlocations.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"custom_locations\";\n\n/**\n * Migrate\n * Extends proxy_host table with locations field\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.json(\"locations\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20190218060101_hsts.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"hsts\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"proxy_host\", (proxy_host) => {\n\t\t\tproxy_host.integer(\"hsts_enabled\").notNull().unsigned().defaultTo(0);\n\t\t\tproxy_host.integer(\"hsts_subdomains\").notNull().unsigned().defaultTo(0);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] proxy_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"redirection_host\", (redirection_host) => {\n\t\t\t\tredirection_host.integer(\"hsts_enabled\").notNull().unsigned().defaultTo(0);\n\t\t\t\tredirection_host.integer(\"hsts_subdomains\").notNull().unsigned().defaultTo(0);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\n\t\t\treturn knex.schema.table(\"dead_host\", (dead_host) => {\n\t\t\t\tdead_host.integer(\"hsts_enabled\").notNull().unsigned().defaultTo(0);\n\t\t\t\tdead_host.integer(\"hsts_subdomains\").notNull().unsigned().defaultTo(0);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] dead_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20190227065017_settings.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"settings\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema.createTable('setting', (table) => {\n\t\ttable.string('id').notNull().primary();\n\t\ttable.string('name', 100).notNull();\n\t\ttable.string('description', 255).notNull();\n\t\ttable.string('value', 255).notNull();\n\t\ttable.json('meta').notNull();\n\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] setting Table created`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down the initial data.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20200410143839_access_list_client.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"access_list_client\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.createTable(\"access_list_client\", (table) => {\n\t\t\ttable.increments().primary();\n\t\t\ttable.dateTime(\"created_on\").notNull();\n\t\t\ttable.dateTime(\"modified_on\").notNull();\n\t\t\ttable.integer(\"access_list_id\").notNull().unsigned();\n\t\t\ttable.string(\"address\").notNull();\n\t\t\ttable.string(\"directive\").notNull();\n\t\t\ttable.json(\"meta\").notNull();\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list_client Table created`);\n\n\t\t\treturn knex.schema.table(\"access_list\", (access_list) => {\n\t\t\t\taccess_list.integer(\"satify_any\").notNull().defaultTo(0);\n\t\t\t});\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema.dropTable(\"access_list_client\").then(() => {\n\t\tlogger.info(`[${migrateName}] access_list_client Table dropped`);\n\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20200410143840_access_list_client_fix.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"access_list_client_fix\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"access_list\", (access_list) => {\n\t\t\taccess_list.renameColumn(\"satify_any\", \"satisfy_any\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst down = (_knex) => {\n\tlogger.warn(`[${migrateName}] You can't migrate down this one.`);\n\treturn Promise.resolve(true);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20201014143841_pass_auth.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"pass_auth\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object}  knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"access_list\", (access_list) => {\n\t\t\taccess_list.integer(\"pass_auth\").notNull().defaultTo(1);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"access_list\", (access_list) => {\n\t\t\taccess_list.dropColumn(\"pass_auth\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] access_list pass_auth Column dropped`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20210210154702_redirection_scheme.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirection_scheme\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", (table) => {\n\t\t\ttable.string(\"forward_scheme\").notNull().defaultTo(\"$scheme\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", (table) => {\n\t\t\ttable.dropColumn(\"forward_scheme\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20210210154703_redirection_status_code.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirection_status_code\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", (table) => {\n\t\t\ttable.integer(\"forward_http_code\").notNull().unsigned().defaultTo(302);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", (table) => {\n\t\t\ttable.dropColumn(\"forward_http_code\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20210423103500_stream_domain.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"stream_domain\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"stream\", (table) => {\n\t\t\ttable.renameColumn(\"forward_ip\", \"forwarding_host\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"stream\", (table) => {\n\t\t\ttable.renameColumn(\"forwarding_host\", \"forward_ip\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table altered`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20211108145214_regenerate_default_host.js",
    "content": "import internalNginx from \"../internal/nginx.js\";\nimport { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"stream_domain\";\n\nasync function regenerateDefaultHost(knex) {\n\tconst row = await knex(\"setting\").select(\"*\").where(\"id\", \"default-site\").first();\n\n\tif (!row) {\n\t\treturn Promise.resolve();\n\t}\n\n\treturn internalNginx\n\t\t.deleteConfig(\"default\")\n\t\t.then(() => {\n\t\t\treturn internalNginx.generateConfig(\"default\", row);\n\t\t})\n\t\t.then(() => {\n\t\t\treturn internalNginx.test();\n\t\t})\n\t\t.then(() => {\n\t\t\treturn internalNginx.reload();\n\t\t});\n}\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn regenerateDefaultHost(knex);\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn regenerateDefaultHost(knex);\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20240427161436_stream_ssl.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"stream_ssl\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"stream\", (table) => {\n\t\t\ttable.integer(\"certificate_id\").notNull().unsigned().defaultTo(0);\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"stream\", (table) => {\n\t\t\ttable.dropColumn(\"certificate_id\");\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] stream Table altered`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20251111090000_redirect_auto_scheme.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirect_auto_scheme\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Up...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", async (table) => {\n\t\t\t// change the column default from $scheme to auto\n\t\t\tawait table.string(\"forward_scheme\").notNull().defaultTo(\"auto\").alter();\n\t\t\tawait knex('redirection_host')\n\t\t\t\t.where('forward_scheme', '$scheme')\n\t\t\t\t.update({ forward_scheme: 'auto' });\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = (knex) => {\n\tlogger.info(`[${migrateName}] Migrating Down...`);\n\n\treturn knex.schema\n\t\t.table(\"redirection_host\", async (table) => {\n\t\t\tawait table.string(\"forward_scheme\").notNull().defaultTo(\"$scheme\").alter();\n\t\t\tawait knex('redirection_host')\n\t\t\t\t.where('forward_scheme', 'auto')\n\t\t\t\t.update({ forward_scheme: '$scheme' });\n\t\t})\n\t\t.then(() => {\n\t\t\tlogger.info(`[${migrateName}] redirection_host Table altered`);\n\t\t});\n};\n\nexport { up, down };\n"
  },
  {
    "path": "backend/migrations/20260131163528_trust_forwarded_proto.js",
    "content": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"trust_forwarded_proto\";\n\n/**\n * Migrate\n *\n * @see http://knexjs.org/#Schema\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst up = function (knex) {\n    logger.info(`[${migrateName}] Migrating Up...`);\n\n    return knex.schema\n        .alterTable('proxy_host', (table) => {\n            table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0);\n        })\n        .then(() => {\n            logger.info(`[${migrateName}] proxy_host Table altered`);\n        });\n};\n\n/**\n * Undo Migrate\n *\n * @param   {Object} knex\n * @returns {Promise}\n */\nconst down = function (knex) {\n    logger.info(`[${migrateName}] Migrating Down...`);\n\n    return knex.schema\n        .alterTable('proxy_host', (table) => {\n            table.dropColumn('trust_forwarded_proto');\n        })\n        .then(() => {\n            logger.info(`[${migrateName}] proxy_host Table altered`);\n        });\n};\n\nexport { up, down };"
  },
  {
    "path": "backend/models/access_list.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport AccessListAuth from \"./access_list_auth.js\";\nimport AccessListClient from \"./access_list_client.js\";\nimport now from \"./now_helper.js\";\nimport ProxyHostModel from \"./proxy_host.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\", \"satisfy_any\", \"pass_auth\"];\n\nclass AccessList extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"AccessList\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"access_list\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\titems: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: AccessListAuth,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list.id\",\n\t\t\t\t\tto: \"access_list_auth.access_list_id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tclients: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: AccessListClient,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list.id\",\n\t\t\t\t\tto: \"access_list_client.access_list_id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tproxy_hosts: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: ProxyHostModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list.id\",\n\t\t\t\t\tto: \"proxy_host.access_list_id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"proxy_host.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default AccessList;\n"
  },
  {
    "path": "backend/models/access_list_auth.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport accessListModel from \"./access_list.js\";\nimport now from \"./now_helper.js\";\n\nModel.knex(db());\n\nclass AccessListAuth extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\tstatic get name() {\n\t\treturn \"AccessListAuth\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"access_list_auth\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\taccess_list: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: accessListModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list_auth.access_list_id\",\n\t\t\t\t\tto: \"access_list.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"access_list.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default AccessListAuth;\n"
  },
  {
    "path": "backend/models/access_list_client.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport accessListModel from \"./access_list.js\";\nimport now from \"./now_helper.js\";\n\nModel.knex(db());\n\nclass AccessListClient extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\tstatic get name() {\n\t\treturn \"AccessListClient\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"access_list_client\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\taccess_list: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: accessListModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"access_list_client.access_list_id\",\n\t\t\t\t\tto: \"access_list.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"access_list.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default AccessListClient;\n"
  },
  {
    "path": "backend/models/audit-log.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nclass AuditLog extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\tstatic get name() {\n\t\treturn \"AuditLog\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"audit_log\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\tuser: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"audit_log.user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default AuditLog;\n"
  },
  {
    "path": "backend/models/auth.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport bcrypt from \"bcrypt\";\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\"];\n\nfunction encryptPassword() {\n\tif (this.type === \"password\" && this.secret) {\n\t\treturn bcrypt.hash(this.secret, 13).then((hash) => {\n\t\t\tthis.secret = hash;\n\t\t});\n\t}\n\n\treturn null;\n}\n\nclass Auth extends Model {\n\t$beforeInsert(queryContext) {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\n\t\treturn encryptPassword.apply(this, queryContext);\n\t}\n\n\t$beforeUpdate(queryContext) {\n\t\tthis.modified_on = now();\n\t\treturn encryptPassword.apply(this, queryContext);\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\t/**\n\t * Verify a plain password against the encrypted password\n\t *\n\t * @param {String} password\n\t * @returns {Promise}\n\t */\n\tverifyPassword(password) {\n\t\treturn bcrypt.compare(password, this.secret);\n\t}\n\n\tstatic get name() {\n\t\treturn \"Auth\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"auth\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\tuser: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"auth.user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tfilter: {\n\t\t\t\t\tis_deleted: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default Auth;\n"
  },
  {
    "path": "backend/models/certificate.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport deadHostModel from \"./dead_host.js\";\nimport now from \"./now_helper.js\";\nimport proxyHostModel from \"./proxy_host.js\";\nimport redirectionHostModel from \"./redirection_host.js\";\nimport streamModel from \"./stream.js\";\nimport userModel from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\"];\n\nclass Certificate extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for expires_on\n\t\tif (typeof this.expires_on === \"undefined\") {\n\t\t\tthis.expires_on = now();\n\t\t}\n\n\t\t// Default for domain_names\n\t\tif (typeof this.domain_names === \"undefined\") {\n\t\t\tthis.domain_names = [];\n\t\t}\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\n\t\tthis.domain_names.sort();\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\n\t\t// Sort domain_names\n\t\tif (typeof this.domain_names !== \"undefined\") {\n\t\t\tthis.domain_names.sort();\n\t\t}\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"Certificate\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"certificate\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"domain_names\", \"meta\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: userModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"certificate.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tproxy_hosts: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: proxyHostModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"certificate.id\",\n\t\t\t\t\tto: \"proxy_host.certificate_id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"proxy_host.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tdead_hosts: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: deadHostModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"certificate.id\",\n\t\t\t\t\tto: \"dead_host.certificate_id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"dead_host.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tredirection_hosts: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: redirectionHostModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"certificate.id\",\n\t\t\t\t\tto: \"redirection_host.certificate_id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"redirection_host.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tstreams: {\n\t\t\t\trelation: Model.HasManyRelation,\n\t\t\t\tmodelClass: streamModel,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"certificate.id\",\n\t\t\t\t\tto: \"stream.certificate_id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"stream.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default Certificate;\n"
  },
  {
    "path": "backend/models/dead_host.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport Certificate from \"./certificate.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\", \"ssl_forced\", \"http2_support\", \"enabled\", \"hsts_enabled\", \"hsts_subdomains\"];\n\nclass DeadHost extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for domain_names\n\t\tif (typeof this.domain_names === \"undefined\") {\n\t\t\tthis.domain_names = [];\n\t\t}\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\n\t\tthis.domain_names.sort();\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\n\t\t// Sort domain_names\n\t\tif (typeof this.domain_names !== \"undefined\") {\n\t\t\tthis.domain_names.sort();\n\t\t}\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"DeadHost\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"dead_host\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"domain_names\", \"meta\"];\n\t}\n\n\tstatic get defaultAllowGraph() {\n\t\treturn \"[owner,certificate]\";\n\t}\n\n\tstatic get defaultExpand() {\n\t\treturn [\"certificate\", \"owner\"];\n\t}\n\n\tstatic get defaultOrder() {\n\t\treturn [castJsonIfNeed(\"domain_names\"), \"ASC\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"dead_host.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tcertificate: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: Certificate,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"dead_host.certificate_id\",\n\t\t\t\t\tto: \"certificate.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"certificate.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default DeadHost;\n"
  },
  {
    "path": "backend/models/now_helper.js",
    "content": "import { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { isSqlite } from \"../lib/config.js\";\n\nModel.knex(db());\n\nexport default () => {\n\tif (isSqlite()) {\n\t\treturn Model.raw(\"datetime('now','localtime')\");\n\t}\n\treturn Model.raw(\"NOW()\");\n};\n"
  },
  {
    "path": "backend/models/proxy_host.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport AccessList from \"./access_list.js\";\nimport Certificate from \"./certificate.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\n\t\"is_deleted\",\n\t\"ssl_forced\",\n\t\"caching_enabled\",\n\t\"block_exploits\",\n\t\"allow_websocket_upgrade\",\n\t\"http2_support\",\n\t\"enabled\",\n\t\"hsts_enabled\",\n\t\"hsts_subdomains\",\n\t\"trust_forwarded_proto\",\n];\n\nclass ProxyHost extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for domain_names\n\t\tif (typeof this.domain_names === \"undefined\") {\n\t\t\tthis.domain_names = [];\n\t\t}\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\n\t\tthis.domain_names.sort();\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\n\t\t// Sort domain_names\n\t\tif (typeof this.domain_names !== \"undefined\") {\n\t\t\tthis.domain_names.sort();\n\t\t}\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"ProxyHost\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"proxy_host\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"domain_names\", \"meta\", \"locations\"];\n\t}\n\n\tstatic get defaultAllowGraph() {\n\t\treturn \"[owner,access_list.[clients,items],certificate]\";\n\t}\n\n\tstatic get defaultExpand() {\n\t\treturn [\"owner\", \"certificate\", \"access_list.[clients,items]\"];\n\t}\n\n\tstatic get defaultOrder() {\n\t\treturn [castJsonIfNeed(\"domain_names\"), \"ASC\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"proxy_host.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\taccess_list: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: AccessList,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"proxy_host.access_list_id\",\n\t\t\t\t\tto: \"access_list.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"access_list.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tcertificate: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: Certificate,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"proxy_host.certificate_id\",\n\t\t\t\t\tto: \"certificate.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"certificate.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default ProxyHost;\n"
  },
  {
    "path": "backend/models/redirection_host.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport Certificate from \"./certificate.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\n\t\"is_deleted\",\n\t\"enabled\",\n\t\"preserve_path\",\n\t\"ssl_forced\",\n\t\"block_exploits\",\n\t\"hsts_enabled\",\n\t\"hsts_subdomains\",\n\t\"http2_support\",\n];\n\nclass RedirectionHost extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for domain_names\n\t\tif (typeof this.domain_names === \"undefined\") {\n\t\t\tthis.domain_names = [];\n\t\t}\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\n\t\tthis.domain_names.sort();\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\n\t\t// Sort domain_names\n\t\tif (typeof this.domain_names !== \"undefined\") {\n\t\t\tthis.domain_names.sort();\n\t\t}\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"RedirectionHost\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"redirection_host\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"domain_names\", \"meta\"];\n\t}\n\n\tstatic get defaultAllowGraph() {\n\t\treturn \"[owner,certificate]\";\n\t}\n\n\tstatic get defaultExpand() {\n\t\treturn [\"certificate\", \"owner\"];\n\t}\n\n\tstatic get defaultOrder() {\n\t\treturn [castJsonIfNeed(\"domain_names\"), \"ASC\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"redirection_host.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tcertificate: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: Certificate,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"redirection_host.certificate_id\",\n\t\t\t\t\tto: \"certificate.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"certificate.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default RedirectionHost;\n"
  },
  {
    "path": "backend/models/setting.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\n\nModel.knex(db());\n\nclass Setting extends Model {\n\t$beforeInsert () {\n\t\t// Default for meta\n\t\tif (typeof this.meta === 'undefined') {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\tstatic get name () {\n\t\treturn 'Setting';\n\t}\n\n\tstatic get tableName () {\n\t\treturn 'setting';\n\t}\n\n\tstatic get jsonAttributes () {\n\t\treturn ['meta'];\n\t}\n}\n\nexport default Setting;\n"
  },
  {
    "path": "backend/models/stream.js",
    "content": "import { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport Certificate from \"./certificate.js\";\nimport now from \"./now_helper.js\";\nimport User from \"./user.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\", \"enabled\", \"tcp_forwarding\", \"udp_forwarding\"];\n\nclass Stream extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for meta\n\t\tif (typeof this.meta === \"undefined\") {\n\t\t\tthis.meta = {};\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"Stream\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"stream\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"meta\"];\n\t}\n\n\tstatic get defaultAllowGraph() {\n\t\treturn \"[owner,certificate]\";\n\t}\n\n\tstatic get defaultExpand() {\n\t\treturn [\"certificate\", \"owner\"];\n\t}\n\n\tstatic get defaultOrder() {\n\t\treturn [castJsonIfNeed(\"incoming_port\"), \"ASC\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\towner: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: User,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"stream.owner_user_id\",\n\t\t\t\t\tto: \"user.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"user.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t\tcertificate: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: Certificate,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"stream.certificate_id\",\n\t\t\t\t\tto: \"certificate.id\",\n\t\t\t\t},\n\t\t\t\tmodify: (qb) => {\n\t\t\t\t\tqb.where(\"certificate.is_deleted\", 0);\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default Stream;\n"
  },
  {
    "path": "backend/models/token.js",
    "content": "/**\n NOTE: This is not a database table, this is a model of a Token object that can be created/loaded\n and then has abilities after that.\n */\n\nimport crypto from \"node:crypto\";\nimport jwt from \"jsonwebtoken\";\nimport _ from \"lodash\";\nimport { getPrivateKey, getPublicKey } from \"../lib/config.js\";\nimport errs from \"../lib/error.js\";\nimport { global as logger } from \"../logger.js\";\n\nconst ALGO = \"RS256\";\n\nexport default () => {\n\tlet tokenData = {};\n\n\tconst self = {\n\t\t/**\n\t\t * @param {Object}  payload\n\t\t * @returns {Promise}\n\t\t */\n\t\tcreate: (payload) => {\n\t\t\tif (!getPrivateKey()) {\n\t\t\t\tlogger.error(\"Private key is empty!\");\n\t\t\t}\n\t\t\t// sign with RSA SHA256\n\t\t\tconst options = {\n\t\t\t\talgorithm: ALGO,\n\t\t\t\texpiresIn: payload.expiresIn || \"1d\",\n\t\t\t};\n\n\t\t\tpayload.jti = crypto.randomBytes(12).toString(\"base64\").substring(-8);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tjwt.sign(payload, getPrivateKey(), options, (err, token) => {\n\t\t\t\t\tif (err) {\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttokenData = payload;\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\ttoken: token,\n\t\t\t\t\t\t\tpayload: payload,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * @param {String} token\n\t\t * @returns {Promise}\n\t\t */\n\t\tload: (token) => {\n\t\t\tif (!getPublicKey()) {\n\t\t\t\tlogger.error(\"Public key is empty!\");\n\t\t\t}\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (!token || token === null || token === \"null\") {\n\t\t\t\t\t\treject(new errs.AuthError(\"Empty token\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjwt.verify(\n\t\t\t\t\t\t\ttoken,\n\t\t\t\t\t\t\tgetPublicKey(),\n\t\t\t\t\t\t\t{ ignoreExpiration: false, algorithms: [ALGO] },\n\t\t\t\t\t\t\t(err, result) => {\n\t\t\t\t\t\t\t\tif (err) {\n\t\t\t\t\t\t\t\t\tif (err.name === \"TokenExpiredError\") {\n\t\t\t\t\t\t\t\t\t\treject(new errs.AuthError(\"Token has expired\", err));\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\treject(err);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\ttokenData = result;\n\n\t\t\t\t\t\t\t\t\t// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.\n\t\t\t\t\t\t\t\t\t// For 30 days at least, we need to replace 'all' with user.\n\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\ttypeof tokenData.scope !== \"undefined\" &&\n\t\t\t\t\t\t\t\t\t\t_.indexOf(tokenData.scope, \"all\") !== -1\n\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\ttokenData.scope = [\"user\"];\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tresolve(tokenData);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\treject(err);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * Does the token have the specified scope?\n\t\t *\n\t\t * @param   {String}  scope\n\t\t * @returns {Boolean}\n\t\t */\n\t\thasScope: (scope) => typeof tokenData.scope !== \"undefined\" && _.indexOf(tokenData.scope, scope) !== -1,\n\n\t\t/**\n\t\t * @param  {String}  key\n\t\t * @return {*}\n\t\t */\n\t\tget: (key) => {\n\t\t\tif (typeof tokenData[key] !== \"undefined\") {\n\t\t\t\treturn tokenData[key];\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\n\t\t/**\n\t\t * @param  {String}  key\n\t\t * @param  {*}       value\n\t\t */\n\t\tset: (key, value) => {\n\t\t\ttokenData[key] = value;\n\t\t},\n\n\t\t/**\n\t\t * @param   [defaultValue]\n\t\t * @returns {Integer}\n\t\t */\n\t\tgetUserId: (defaultValue) => {\n\t\t\tconst attrs = self.get(\"attrs\");\n\t\t\tif (attrs?.id) {\n\t\t\t\treturn attrs.id;\n\t\t\t}\n\n\t\t\treturn defaultValue || 0;\n\t\t},\n\t};\n\n\treturn self;\n};\n"
  },
  {
    "path": "backend/models/user.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { convertBoolFieldsToInt, convertIntFieldsToBool } from \"../lib/helpers.js\";\nimport now from \"./now_helper.js\";\nimport UserPermission from \"./user_permission.js\";\n\nModel.knex(db());\n\nconst boolFields = [\"is_deleted\", \"is_disabled\"];\n\nclass User extends Model {\n\t$beforeInsert() {\n\t\tthis.created_on = now();\n\t\tthis.modified_on = now();\n\n\t\t// Default for roles\n\t\tif (typeof this.roles === \"undefined\") {\n\t\t\tthis.roles = [];\n\t\t}\n\t}\n\n\t$beforeUpdate() {\n\t\tthis.modified_on = now();\n\t}\n\n\t$parseDatabaseJson(json) {\n\t\tconst thisJson = super.$parseDatabaseJson(json);\n\t\treturn convertIntFieldsToBool(thisJson, boolFields);\n\t}\n\n\t$formatDatabaseJson(json) {\n\t\tconst thisJson = convertBoolFieldsToInt(json, boolFields);\n\t\treturn super.$formatDatabaseJson(thisJson);\n\t}\n\n\tstatic get name() {\n\t\treturn \"User\";\n\t}\n\n\tstatic get tableName() {\n\t\treturn \"user\";\n\t}\n\n\tstatic get jsonAttributes() {\n\t\treturn [\"roles\"];\n\t}\n\n\tstatic get relationMappings() {\n\t\treturn {\n\t\t\tpermissions: {\n\t\t\t\trelation: Model.HasOneRelation,\n\t\t\t\tmodelClass: UserPermission,\n\t\t\t\tjoin: {\n\t\t\t\t\tfrom: \"user.id\",\n\t\t\t\t\tto: \"user_permission.user_id\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default User;\n"
  },
  {
    "path": "backend/models/user_permission.js",
    "content": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js\";\nimport now from \"./now_helper.js\";\n\nModel.knex(db());\n\nclass UserPermission extends Model {\n\t$beforeInsert () {\n\t\tthis.created_on  = now();\n\t\tthis.modified_on = now();\n\t}\n\n\t$beforeUpdate () {\n\t\tthis.modified_on = now();\n\t}\n\n\tstatic get name () {\n\t\treturn 'UserPermission';\n\t}\n\n\tstatic get tableName () {\n\t\treturn 'user_permission';\n\t}\n}\n\nexport default UserPermission;\n"
  },
  {
    "path": "backend/nodemon.json",
    "content": "{\n  \"verbose\": false,\n  \"ignore\": [\n    \"data\"\n  ],\n  \"ext\": \"js json ejs cjs\"\n}\n"
  },
  {
    "path": "backend/package.json",
    "content": "{\n\t\"name\": \"nginx-proxy-manager\",\n\t\"version\": \"2.0.0\",\n\t\"description\": \"A beautiful interface for creating Nginx endpoints\",\n\t\"author\": \"Jamie Curnow <jc@jc21.com>\",\n\t\"license\": \"MIT\",\n\t\"main\": \"index.js\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"lint\": \"biome lint\",\n\t\t\"prettier\": \"biome format --write .\",\n\t\t\"validate-schema\": \"node validate-schema.js\",\n\t\t\"regenerate-config\": \"node scripts/regenerate-config\"\n\t},\n\t\"dependencies\": {\n\t\t\"@apidevtools/json-schema-ref-parser\": \"^15.3.1\",\n\t\t\"ajv\": \"^8.18.0\",\n\t\t\"archiver\": \"^7.0.1\",\n\t\t\"batchflow\": \"^0.4.0\",\n\t\t\"bcrypt\": \"^6.0.0\",\n\t\t\"better-sqlite3\": \"^12.6.2\",\n\t\t\"body-parser\": \"^2.2.2\",\n\t\t\"compression\": \"^1.8.1\",\n\t\t\"express\": \"^5.2.1\",\n\t\t\"express-fileupload\": \"^1.5.2\",\n\t\t\"gravatar\": \"^1.8.2\",\n\t\t\"jsonwebtoken\": \"^9.0.3\",\n\t\t\"knex\": \"3.1.0\",\n\t\t\"liquidjs\": \"10.24.0\",\n\t\t\"lodash\": \"^4.17.23\",\n\t\t\"moment\": \"^2.30.1\",\n\t\t\"mysql2\": \"^3.18.2\",\n\t\t\"node-rsa\": \"^1.1.1\",\n\t\t\"objection\": \"3.1.5\",\n\t\t\"otplib\": \"^13.3.0\",\n\t\t\"path\": \"^0.12.7\",\n\t\t\"pg\": \"^8.19.0\",\n\t\t\"proxy-agent\": \"^6.5.0\",\n\t\t\"signale\": \"1.4.0\",\n\t\t\"sqlite3\": \"^5.1.7\",\n\t\t\"temp-write\": \"^6.0.1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@apidevtools/swagger-parser\": \"^12.1.0\",\n\t\t\"@biomejs/biome\": \"^2.4.5\",\n\t\t\"chalk\": \"5.6.2\",\n\t\t\"nodemon\": \"^3.1.14\"\n\t},\n\t\"signale\": {\n\t\t\"displayDate\": true,\n\t\t\"displayTimestamp\": true\n\t}\n}\n"
  },
  {
    "path": "backend/routes/audit-log.js",
    "content": "import express from \"express\";\nimport internalAuditLog from \"../internal/audit-log.js\";\nimport jwtdecode from \"../lib/express/jwt-decode.js\";\nimport validator from \"../lib/validator/index.js\";\nimport { debug, express as logger } from \"../logger.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/audit-log\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/audit-log\n\t *\n\t * Retrieve all logs\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific audit log entry\n *\n * /api/audit-log/123\n */\nrouter\n\t.route(\"/:event_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/audit-log/123\n\t *\n\t * Retrieve a specific entry\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"event_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tevent_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tevent_id: req.params.event_id,\n\t\t\t\t\texpand:\n\t\t\t\t\t\ttypeof req.query.expand === \"string\"\n\t\t\t\t\t\t\t? req.query.expand.split(\",\")\n\t\t\t\t\t\t\t: null,\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst item = await internalAuditLog.get(res.locals.access, {\n\t\t\t\tid: data.event_id,\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(item);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/main.js",
    "content": "import express from \"express\";\nimport errs from \"../lib/error.js\";\nimport pjson from \"../package.json\" with { type: \"json\" };\nimport { isSetup } from \"../setup.js\";\nimport auditLogRoutes from \"./audit-log.js\";\nimport accessListsRoutes from \"./nginx/access_lists.js\";\nimport certificatesHostsRoutes from \"./nginx/certificates.js\";\nimport deadHostsRoutes from \"./nginx/dead_hosts.js\";\nimport proxyHostsRoutes from \"./nginx/proxy_hosts.js\";\nimport redirectionHostsRoutes from \"./nginx/redirection_hosts.js\";\nimport streamsRoutes from \"./nginx/streams.js\";\nimport reportsRoutes from \"./reports.js\";\nimport schemaRoutes from \"./schema.js\";\nimport settingsRoutes from \"./settings.js\";\nimport tokensRoutes from \"./tokens.js\";\nimport usersRoutes from \"./users.js\";\nimport versionRoutes from \"./version.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * Health Check\n * GET /api\n */\nrouter.get(\"/\", async (_, res /*, next*/) => {\n\tconst version = pjson.version.split(\"-\").shift().split(\".\");\n\tconst setup = await isSetup();\n\n\tres.status(200).send({\n\t\tstatus: \"OK\",\n\t\tsetup,\n\t\tversion: {\n\t\t\tmajor: Number.parseInt(version.shift(), 10),\n\t\t\tminor: Number.parseInt(version.shift(), 10),\n\t\t\trevision: Number.parseInt(version.shift(), 10),\n\t\t},\n\t});\n});\n\nrouter.use(\"/schema\", schemaRoutes);\nrouter.use(\"/tokens\", tokensRoutes);\nrouter.use(\"/users\", usersRoutes);\nrouter.use(\"/audit-log\", auditLogRoutes);\nrouter.use(\"/reports\", reportsRoutes);\nrouter.use(\"/settings\", settingsRoutes);\nrouter.use(\"/version\", versionRoutes);\nrouter.use(\"/nginx/proxy-hosts\", proxyHostsRoutes);\nrouter.use(\"/nginx/redirection-hosts\", redirectionHostsRoutes);\nrouter.use(\"/nginx/dead-hosts\", deadHostsRoutes);\nrouter.use(\"/nginx/streams\", streamsRoutes);\nrouter.use(\"/nginx/access-lists\", accessListsRoutes);\nrouter.use(\"/nginx/certificates\", certificatesHostsRoutes);\n\n/**\n * API 404 for all other routes\n *\n * ALL /api/*\n */\nrouter.all(/(.+)/, (req, _, next) => {\n\treq.params.page = req.params[\"0\"];\n\tnext(new errs.ItemNotFoundError(req.params.page));\n});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/access_lists.js",
    "content": "import express from \"express\";\nimport internalAccessList from \"../../internal/access-list.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/access-lists\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/access-lists\n\t *\n\t * Retrieve all access-lists\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/access-lists\n\t *\n\t * Create a new access-list\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/access-lists\", \"post\"), req.body);\n\t\t\tconst result = await internalAccessList.create(res.locals.access, payload);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific access-list\n *\n * /api/nginx/access-lists/123\n */\nrouter\n\t.route(\"/:list_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/access-lists/123\n\t *\n\t * Retrieve a specific access-list\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"list_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tlist_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlist_id: req.params.list_id,\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalAccessList.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.list_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/nginx/access-lists/123\n\t *\n\t * Update and existing access-list\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/access-lists/{listID}\", \"put\"), req.body);\n\t\t\tpayload.id = Number.parseInt(req.params.list_id, 10);\n\t\t\tconst result = await internalAccessList.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/access-lists/123\n\t *\n\t * Delete and existing access-list\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalAccessList.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.list_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/certificates.js",
    "content": "import express from \"express\";\nimport dnsPlugins from \"../../certbot/dns-plugins.json\" with { type: \"json\" };\nimport internalCertificate from \"../../internal/certificate.js\";\nimport errs from \"../../lib/error.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/certificates\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/certificates\n\t *\n\t * Retrieve all certificates\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand:\n\t\t\t\t\t\ttypeof req.query.expand === \"string\"\n\t\t\t\t\t\t\t? req.query.expand.split(\",\")\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalCertificate.getAll(\n\t\t\t\tres.locals.access,\n\t\t\t\tdata.expand,\n\t\t\t\tdata.query,\n\t\t\t);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/certificates\n\t *\n\t * Create a new certificate\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/nginx/certificates\", \"post\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\treq.setTimeout(900000); // 15 minutes timeout\n\t\t\tconst result = await internalCertificate.create(\n\t\t\t\tres.locals.access,\n\t\t\t\tpayload,\n\t\t\t);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * /api/nginx/certificates/dns-providers\n */\nrouter\n\t.route(\"/dns-providers\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/certificates/dns-providers\n\t *\n\t * Get list of all supported DNS providers\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tif (!res.locals.access.token.getUserId()) {\n\t\t\t\tthrow new errs.PermissionError(\"Login required\");\n\t\t\t}\n\t\t\tconst clean = Object.keys(dnsPlugins).map((key) => ({\n\t\t\t\tid: key,\n\t\t\t\tname: dnsPlugins[key].name,\n\t\t\t\tcredentials: dnsPlugins[key].credentials,\n\t\t\t}));\n\n\t\t\tclean.sort((a, b) => a.name.localeCompare(b.name));\n\t\t\tres.status(200).send(clean);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Test HTTP challenge for domains\n *\n * /api/nginx/certificates/test-http\n */\nrouter\n\t.route(\"/test-http\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/certificates/test-http\n\t *\n\t * Test HTTP challenge for domains\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/nginx/certificates/test-http\", \"post\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\treq.setTimeout(60000); // 1 minute timeout\n\n\t\t\tconst result = await internalCertificate.testHttpsChallenge(\n\t\t\t\tres.locals.access,\n\t\t\t\tpayload,\n\t\t\t);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Validate Certs before saving\n *\n * /api/nginx/certificates/validate\n */\nrouter\n\t.route(\"/validate\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/certificates/validate\n\t *\n\t * Validate certificates\n\t */\n\t.post(async (req, res, next) => {\n\t\tif (!req.files) {\n\t\t\tres.status(400).send({ error: \"No files were uploaded\" });\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await internalCertificate.validate({\n\t\t\t\tfiles: req.files,\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific certificate\n *\n * /api/nginx/certificates/123\n */\nrouter\n\t.route(\"/:certificate_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/certificates/123\n\t *\n\t * Retrieve a specific certificate\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"certificate_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tcertificate_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcertificate_id: req.params.certificate_id,\n\t\t\t\t\texpand:\n\t\t\t\t\t\ttypeof req.query.expand === \"string\"\n\t\t\t\t\t\t\t? req.query.expand.split(\",\")\n\t\t\t\t\t\t\t: null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalCertificate.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.certificate_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/certificates/123\n\t *\n\t * Update and existing certificate\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalCertificate.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.certificate_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Upload Certs\n *\n * /api/nginx/certificates/123/upload\n */\nrouter\n\t.route(\"/:certificate_id/upload\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/certificates/123/upload\n\t *\n\t * Upload certificates\n\t */\n\t.post(async (req, res, next) => {\n\t\tif (!req.files) {\n\t\t\tres.status(400).send({ error: \"No files were uploaded\" });\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await internalCertificate.upload(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.certificate_id, 10),\n\t\t\t\tfiles: req.files,\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Renew LE Certs\n *\n * /api/nginx/certificates/123/renew\n */\nrouter\n\t.route(\"/:certificate_id/renew\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/certificates/123/renew\n\t *\n\t * Renew certificate\n\t */\n\t.post(async (req, res, next) => {\n\t\treq.setTimeout(900000); // 15 minutes timeout\n\t\ttry {\n\t\t\tconst result = await internalCertificate.renew(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.certificate_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Download LE Certs\n *\n * /api/nginx/certificates/123/download\n */\nrouter\n\t.route(\"/:certificate_id/download\")\n\t.options((_req, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/certificates/123/download\n\t *\n\t * Renew certificate\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalCertificate.download(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.certificate_id, 10),\n\t\t\t});\n\t\t\tres.status(200).download(result.fileName);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/dead_hosts.js",
    "content": "import express from \"express\";\nimport internalDeadHost from \"../../internal/dead-host.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/dead-hosts\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/dead-hosts\n\t *\n\t * Retrieve all dead-hosts\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalDeadHost.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/dead-hosts\n\t *\n\t * Create a new dead-host\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/dead-hosts\", \"post\"), req.body);\n\t\t\tconst result = await internalDeadHost.create(res.locals.access, payload);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific dead-host\n *\n * /api/nginx/dead-hosts/123\n */\nrouter\n\t.route(\"/:host_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/dead-hosts/123\n\t *\n\t * Retrieve a specific dead-host\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"host_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\thost_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\thost_id: req.params.host_id,\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalDeadHost.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.host_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/nginx/dead-hosts/123\n\t *\n\t * Update an existing dead-host\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/dead-hosts/{hostID}\", \"put\"), req.body);\n\t\t\tpayload.id = Number.parseInt(req.params.host_id, 10);\n\t\t\tconst result = await internalDeadHost.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/dead-hosts/123\n\t *\n\t * Delete a dead-host\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalDeadHost.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Enable dead-host\n *\n * /api/nginx/dead-hosts/123/enable\n */\nrouter\n\t.route(\"/:host_id/enable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/dead-hosts/123/enable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalDeadHost.enable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Disable dead-host\n *\n * /api/nginx/dead-hosts/123/disable\n */\nrouter\n\t.route(\"/:host_id/disable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/dead-hosts/123/disable\n\t */\n\t.post((req, res, next) => {\n\t\ttry {\n\t\t\tconst result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) });\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/proxy_hosts.js",
    "content": "import express from \"express\";\nimport internalProxyHost from \"../../internal/proxy-host.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/proxy-hosts\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/proxy-hosts\n\t *\n\t * Retrieve all proxy-hosts\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalProxyHost.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/proxy-hosts\n\t *\n\t * Create a new proxy-host\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/proxy-hosts\", \"post\"), req.body);\n\t\t\tconst result = await internalProxyHost.create(res.locals.access, payload);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err} ${JSON.stringify(err.debug, null, 2)}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific proxy-host\n *\n * /api/nginx/proxy-hosts/123\n */\nrouter\n\t.route(\"/:host_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/proxy-hosts/123\n\t *\n\t * Retrieve a specific proxy-host\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"host_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\thost_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\thost_id: req.params.host_id,\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalProxyHost.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.host_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/nginx/proxy-hosts/123\n\t *\n\t * Update and existing proxy-host\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/proxy-hosts/{hostID}\", \"put\"), req.body);\n\t\t\tpayload.id = Number.parseInt(req.params.host_id, 10);\n\t\t\tconst result = await internalProxyHost.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/proxy-hosts/123\n\t *\n\t * Update and existing proxy-host\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalProxyHost.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Enable proxy-host\n *\n * /api/nginx/proxy-hosts/123/enable\n */\nrouter\n\t.route(\"/:host_id/enable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/proxy-hosts/123/enable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalProxyHost.enable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Disable proxy-host\n *\n * /api/nginx/proxy-hosts/123/disable\n */\nrouter\n\t.route(\"/:host_id/disable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/proxy-hosts/123/disable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalProxyHost.disable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/redirection_hosts.js",
    "content": "import express from \"express\";\nimport internalRedirectionHost from \"../../internal/redirection-host.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/redirection-hosts\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/redirection-hosts\n\t *\n\t * Retrieve all redirection-hosts\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/redirection-hosts\n\t *\n\t * Create a new redirection-host\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/redirection-hosts\", \"post\"), req.body);\n\t\t\tconst result = await internalRedirectionHost.create(res.locals.access, payload);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific redirection-host\n *\n * /api/nginx/redirection-hosts/123\n */\nrouter\n\t.route(\"/:host_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/nginx/redirection-hosts/123\n\t *\n\t * Retrieve a specific redirection-host\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"host_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\thost_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\thost_id: req.params.host_id,\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalRedirectionHost.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.host_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/nginx/redirection-hosts/123\n\t *\n\t * Update and existing redirection-host\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/nginx/redirection-hosts/{hostID}\", \"put\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tpayload.id = Number.parseInt(req.params.host_id, 10);\n\t\t\tconst result = await internalRedirectionHost.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/redirection-hosts/123\n\t *\n\t * Update and existing redirection-host\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalRedirectionHost.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Enable redirection-host\n *\n * /api/nginx/redirection-hosts/123/enable\n */\nrouter\n\t.route(\"/:host_id/enable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/redirection-hosts/123/enable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalRedirectionHost.enable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Disable redirection-host\n *\n * /api/nginx/redirection-hosts/123/disable\n */\nrouter\n\t.route(\"/:host_id/disable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/redirection-hosts/123/disable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalRedirectionHost.disable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/nginx/streams.js",
    "content": "import express from \"express\";\nimport internalStream from \"../../internal/stream.js\";\nimport jwtdecode from \"../../lib/express/jwt-decode.js\";\nimport apiValidator from \"../../lib/validator/api.js\";\nimport validator from \"../../lib/validator/index.js\";\nimport { debug, express as logger } from \"../../logger.js\";\nimport { getValidationSchema } from \"../../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/nginx/streams\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes\n\n\t/**\n\t * GET /api/nginx/streams\n\t *\n\t * Retrieve all streams\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst rows = await internalStream.getAll(res.locals.access, data.expand, data.query);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/nginx/streams\n\t *\n\t * Create a new stream\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/streams\", \"post\"), req.body);\n\t\t\tconst result = await internalStream.create(res.locals.access, payload);\n\t\t\tres.status(201).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific stream\n *\n * /api/nginx/streams/123\n */\nrouter\n\t.route(\"/:stream_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes\n\n\t/**\n\t * GET /api/nginx/streams/123\n\t *\n\t * Retrieve a specific stream\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"stream_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tstream_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstream_id: req.params.stream_id,\n\t\t\t\t\texpand: typeof req.query.expand === \"string\" ? req.query.expand.split(\",\") : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalStream.get(res.locals.access, {\n\t\t\t\tid: Number.parseInt(data.stream_id, 10),\n\t\t\t\texpand: data.expand,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/nginx/streams/123\n\t *\n\t * Update and existing stream\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/nginx/streams/{streamID}\", \"put\"), req.body);\n\t\t\tpayload.id = Number.parseInt(req.params.stream_id, 10);\n\t\t\tconst result = await internalStream.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/nginx/streams/123\n\t *\n\t * Update and existing stream\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalStream.delete(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.stream_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Enable stream\n *\n * /api/nginx/streams/123/enable\n */\nrouter\n\t.route(\"/:host_id/enable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/streams/123/enable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalStream.enable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Disable stream\n *\n * /api/nginx/streams/123/disable\n */\nrouter\n\t.route(\"/:host_id/disable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/nginx/streams/123/disable\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalStream.disable(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.host_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/reports.js",
    "content": "import express from \"express\";\nimport internalReport from \"../internal/report.js\";\nimport jwtdecode from \"../lib/express/jwt-decode.js\";\nimport { debug, express as logger } from \"../logger.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\nrouter\n\t.route(\"/hosts\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /reports/hosts\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await internalReport.getHostsReport(res.locals.access);\n\t\t\tres.status(200).send(data);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/schema.js",
    "content": "import express from \"express\";\nimport { debug, express as logger } from \"../logger.js\";\nimport PACKAGE from \"../package.json\" with { type: \"json\" };\nimport { getCompiledSchema } from \"../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\n\t/**\n\t * GET /schema\n\t */\n\t.get(async (req, res) => {\n\t\ttry {\n\t\t\tconst swaggerJSON = await getCompiledSchema();\n\n\t\t\tlet proto = req.protocol;\n\t\t\tif (typeof req.headers[\"x-forwarded-proto\"] !== \"undefined\" && req.headers[\"x-forwarded-proto\"]) {\n\t\t\t\tproto = req.headers[\"x-forwarded-proto\"];\n\t\t\t}\n\n\t\t\tlet origin = `${proto}://${req.hostname}`;\n\t\t\tif (typeof req.headers.origin !== \"undefined\" && req.headers.origin) {\n\t\t\t\torigin = req.headers.origin;\n\t\t\t}\n\n\t\t\tswaggerJSON.info.version = PACKAGE.version;\n\t\t\tswaggerJSON.servers[0].url = `${origin}/api`;\n\t\t\tres.status(200).send(swaggerJSON);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/settings.js",
    "content": "import express from \"express\";\nimport internalSetting from \"../internal/setting.js\";\nimport jwtdecode from \"../lib/express/jwt-decode.js\";\nimport apiValidator from \"../lib/validator/api.js\";\nimport validator from \"../lib/validator/index.js\";\nimport { debug, express as logger } from \"../logger.js\";\nimport { getValidationSchema } from \"../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/settings\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/settings\n\t *\n\t * Retrieve all settings\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst rows = await internalSetting.getAll(res.locals.access);\n\t\t\tres.status(200).send(rows);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific setting\n *\n * /api/settings/something\n */\nrouter\n\t.route(\"/:setting_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /settings/something\n\t *\n\t * Retrieve a specific setting\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"setting_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tsetting_id: {\n\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\tminLength: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tsetting_id: req.params.setting_id,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst row = await internalSetting.get(res.locals.access, {\n\t\t\t\tid: data.setting_id,\n\t\t\t});\n\t\t\tres.status(200).send(row);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/settings/something\n\t *\n\t * Update and existing setting\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(getValidationSchema(\"/settings/{settingID}\", \"put\"), req.body);\n\t\t\tpayload.id = req.params.setting_id;\n\t\t\tconst result = await internalSetting.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/tokens.js",
    "content": "import express from \"express\";\nimport internalToken from \"../internal/token.js\";\nimport jwtdecode from \"../lib/express/jwt-decode.js\";\nimport apiValidator from \"../lib/validator/api.js\";\nimport { debug, express as logger } from \"../logger.js\";\nimport { getValidationSchema } from \"../schema/index.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\n\t/**\n\t * GET /tokens\n\t *\n\t * Get a new Token, given they already have a token they want to refresh\n\t * We also piggy back on to this method, allowing admins to get tokens\n\t * for services like Job board and Worker.\n\t */\n\t.get(jwtdecode(), async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await internalToken.getFreshToken(res.locals.access, {\n\t\t\t\texpiry: typeof req.query.expiry !== \"undefined\" ? req.query.expiry : null,\n\t\t\t\tscope: typeof req.query.scope !== \"undefined\" ? req.query.scope : null,\n\t\t\t});\n\t\t\tres.status(200).send(data);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /tokens\n\t *\n\t * Create a new Token\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await apiValidator(getValidationSchema(\"/tokens\", \"post\"), req.body);\n\t\t\tconst result = await internalToken.getTokenFromEmail(data);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nrouter\n\t.route(\"/2fa\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\n\t/**\n\t * POST /tokens/2fa\n\t *\n\t * Verify 2FA code and get full token\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst { challenge_token, code } = await apiValidator(getValidationSchema(\"/tokens/2fa\", \"post\"), req.body);\n\t\t\tconst result = await internalToken.verify2FA(challenge_token, code);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/users.js",
    "content": "import express from \"express\";\nimport internal2FA from \"../internal/2fa.js\";\nimport internalUser from \"../internal/user.js\";\nimport Access from \"../lib/access.js\";\nimport { isCI } from \"../lib/config.js\";\nimport errs from \"../lib/error.js\";\nimport jwtdecode from \"../lib/express/jwt-decode.js\";\nimport userIdFromMe from \"../lib/express/user-id-from-me.js\";\nimport apiValidator from \"../lib/validator/api.js\";\nimport validator from \"../lib/validator/index.js\";\nimport { debug, express as logger } from \"../logger.js\";\nimport { getValidationSchema } from \"../schema/index.js\";\nimport { isSetup } from \"../setup.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/users\n */\nrouter\n\t.route(\"/\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * GET /api/users\n\t *\n\t * Retrieve all users\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tquery: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\texpand:\n\t\t\t\t\t\ttypeof req.query.expand === \"string\"\n\t\t\t\t\t\t\t? req.query.expand.split(\",\")\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tquery: typeof req.query.query === \"string\" ? req.query.query : null,\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst users = await internalUser.getAll(\n\t\t\t\tres.locals.access,\n\t\t\t\tdata.expand,\n\t\t\t\tdata.query,\n\t\t\t);\n\t\t\tres.status(200).send(users);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * POST /api/users\n\t *\n\t * Create a new User\n\t */\n\t.post(async (req, res, next) => {\n\t\tconst body = req.body;\n\n\t\ttry {\n\t\t\t// If we are in setup mode, we don't check access for current user\n\t\t\tconst setup = await isSetup();\n\t\t\tif (!setup) {\n\t\t\t\tlogger.info(\"Creating a new user in setup mode\");\n\t\t\t\tconst access = new Access(null);\n\t\t\t\tawait access.load(true);\n\t\t\t\tres.locals.access = access;\n\n\t\t\t\t// We are in setup mode, set some defaults for this first new user, such as making\n\t\t\t\t// them an admin.\n\t\t\t\tbody.is_disabled = false;\n\t\t\t\tif (typeof body.roles !== \"object\" || body.roles === null) {\n\t\t\t\t\tbody.roles = [];\n\t\t\t\t}\n\t\t\t\tif (body.roles.indexOf(\"admin\") === -1) {\n\t\t\t\t\tbody.roles.push(\"admin\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users\", \"post\"),\n\t\t\t\tbody,\n\t\t\t);\n\t\t\tconst user = await internalUser.create(res.locals.access, payload);\n\t\t\tres.status(201).send(user);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/users\n\t *\n\t * Deletes ALL users. This is NOT GENERALLY AVAILABLE!\n\t * (!) It is NOT an authenticated endpoint.\n\t * (!) Only CI should be able to call this endpoint. As a result,\n\t *\n\t * it will only work when the env vars DEBUG=true and CI=true\n\t *\n\t * Do NOT set those env vars in a production environment!\n\t */\n\t.delete(async (_, res, next) => {\n\t\tif (isCI()) {\n\t\t\ttry {\n\t\t\t\tlogger.warn(\"Deleting all users - CI environment detected, allowing this operation\");\n\t\t\t\tawait internalUser.deleteAll();\n\t\t\t\tres.status(200).send(true);\n\t\t\t} catch (err) {\n\t\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\t\tnext(err);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tnext(new errs.ItemNotFoundError());\n\t});\n\n/**\n * Specific user\n *\n * /api/users/123\n */\nrouter\n\t.route(\"/:user_id\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * GET /users/123 or /users/me\n\t *\n\t * Retrieve a specific user\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst data = await validator(\n\t\t\t\t{\n\t\t\t\t\trequired: [\"user_id\"],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tuser_id: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\texpand: {\n\t\t\t\t\t\t\t$ref: \"common#/properties/expand\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tuser_id: req.params.user_id,\n\t\t\t\t\texpand:\n\t\t\t\t\t\ttypeof req.query.expand === \"string\"\n\t\t\t\t\t\t\t? req.query.expand.split(\",\")\n\t\t\t\t\t\t\t: null,\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst user = await internalUser.get(res.locals.access, {\n\t\t\t\tid: data.user_id,\n\t\t\t\texpand: data.expand,\n\t\t\t\tomit: internalUser.getUserOmisionsByAccess(\n\t\t\t\t\tres.locals.access,\n\t\t\t\t\tdata.user_id,\n\t\t\t\t),\n\t\t\t});\n\t\t\tres.status(200).send(user);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * PUT /api/users/123\n\t *\n\t * Update and existing user\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users/{userID}\", \"put\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tpayload.id = req.params.user_id;\n\t\t\tconst result = await internalUser.update(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/users/123\n\t *\n\t * Update and existing user\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalUser.delete(res.locals.access, {\n\t\t\t\tid: req.params.user_id,\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific user auth\n *\n * /api/users/123/auth\n */\nrouter\n\t.route(\"/:user_id/auth\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * PUT /api/users/123/auth\n\t *\n\t * Update password for a user\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users/{userID}/auth\", \"put\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tpayload.id = req.params.user_id;\n\t\t\tconst result = await internalUser.setPassword(res.locals.access, payload);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific user permissions\n *\n * /api/users/123/permissions\n */\nrouter\n\t.route(\"/:user_id/permissions\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * PUT /api/users/123/permissions\n\t *\n\t * Set some or all permissions for a user\n\t */\n\t.put(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst payload = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users/{userID}/permissions\", \"put\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tpayload.id = req.params.user_id;\n\t\t\tconst result = await internalUser.setPermissions(\n\t\t\t\tres.locals.access,\n\t\t\t\tpayload,\n\t\t\t);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * Specific user login as\n *\n * /api/users/123/login\n */\nrouter\n\t.route(\"/:user_id/login\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\n\t/**\n\t * POST /api/users/123/login\n\t *\n\t * Log in as a user\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internalUser.loginAs(res.locals.access, {\n\t\t\t\tid: Number.parseInt(req.params.user_id, 10),\n\t\t\t});\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * User 2FA status\n *\n * /api/users/123/2fa\n */\nrouter\n\t.route(\"/:user_id/2fa\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * POST /api/users/123/2fa\n\t *\n\t * Start 2FA setup, returns QR code URL\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst result = await internal2FA.startSetup(res.locals.access, req.params.user_id);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * GET /api/users/123/2fa\n\t *\n\t * Get 2FA status for a user\n\t */\n\t.get(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst status = await internal2FA.getStatus(res.locals.access, req.params.user_id);\n\t\t\tres.status(200).send(status);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t})\n\n\t/**\n\t * DELETE /api/users/123/2fa?code=XXXXXX\n\t *\n\t * Disable 2FA for a user\n\t */\n\t.delete(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst code = typeof req.query.code === \"string\" ? req.query.code : null;\n\t\t\tif (!code) {\n\t\t\t\tthrow new errs.ValidationError(\"Missing required parameter: code\");\n\t\t\t}\n\t\t\tawait internal2FA.disable(res.locals.access, req.params.user_id, code);\n\t\t\tres.status(200).send(true);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * User 2FA enable\n *\n * /api/users/123/2fa/enable\n */\nrouter\n\t.route(\"/:user_id/2fa/enable\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * POST /api/users/123/2fa/enable\n\t *\n\t * Verify code and enable 2FA\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst { code } = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users/{userID}/2fa/enable\", \"post\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tconst result = await internal2FA.enable(res.locals.access, req.params.user_id, code);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\n/**\n * User 2FA backup codes\n *\n * /api/users/123/2fa/backup-codes\n */\nrouter\n\t.route(\"/:user_id/2fa/backup-codes\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\t.all(jwtdecode())\n\t.all(userIdFromMe)\n\n\t/**\n\t * POST /api/users/123/2fa/backup-codes\n\t *\n\t * Regenerate backup codes\n\t */\n\t.post(async (req, res, next) => {\n\t\ttry {\n\t\t\tconst { code } = await apiValidator(\n\t\t\t\tgetValidationSchema(\"/users/{userID}/2fa/backup-codes\", \"post\"),\n\t\t\t\treq.body,\n\t\t\t);\n\t\t\tconst result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);\n\t\t\tres.status(200).send(result);\n\t\t} catch (err) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);\n\t\t\tnext(err);\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/routes/version.js",
    "content": "import express from \"express\";\nimport internalRemoteVersion from \"../internal/remote-version.js\";\nimport { debug, express as logger } from \"../logger.js\";\n\nconst router = express.Router({\n\tcaseSensitive: true,\n\tstrict: true,\n\tmergeParams: true,\n});\n\n/**\n * /api/version/check\n */\nrouter\n\t.route(\"/check\")\n\t.options((_, res) => {\n\t\tres.sendStatus(204);\n\t})\n\n\t/**\n\t * GET /api/version/check\n\t *\n\t * Check for available updates\n\t */\n\t.get(async (req, res, _next) => {\n\t\ttry {\n\t\t\tconst data = await internalRemoteVersion.get();\n\t\t\tres.status(200).send(data);\n\t\t} catch (error) {\n\t\t\tdebug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);\n\t\t\t// Send 200 even though there's an error to avoid triggering update checks repeatedly\n\t\t\tres.status(200).send({\n\t\t\t\tcurrent: null,\n\t\t\t\tlatest: null,\n\t\t\t\tupdate_available: false,\n\t\t\t});\n\t\t}\n\t});\n\nexport default router;\n"
  },
  {
    "path": "backend/schema/common.json",
    "content": "{\n\t\"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n\t\"$id\": \"common\",\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"description\": \"Unique identifier\",\n\t\t\t\"readOnly\": true,\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"example\": 11\n\t\t},\n\t\t\"expand\": {\n\t\t\t\"anyOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"minItems\": 1,\n\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"query\": {\n\t\t\t\"anyOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\"maxLength\": 255\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"description\": \"Date and time of creation\",\n\t\t\t\"readOnly\": true,\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"2025-10-28T04:17:54.000Z\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"description\": \"Date and time of last update\",\n\t\t\t\"readOnly\": true,\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"2025-10-28T04:17:54.000Z\"\n\t\t},\n\t\t\"user_id\": {\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"example\": 2\n\t\t},\n\t\t\"certificate_id\": {\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"anyOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\"minimum\": 0,\n\t\t\t\t\t\"example\": 5\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"pattern\": \"^new$\",\n\t\t\t\t\t\"example\": \"new\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": 5\n\t\t},\n\t\t\"access_list_id\": {\n\t\t\t\"description\": \"Access List ID\",\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 0,\n\t\t\t\"example\": 3\n\t\t},\n\t\t\"domain_names\": {\n\t\t\t\"description\": \"Domain Names separated by a comma\",\n\t\t\t\"type\": \"array\",\n\t\t\t\"minItems\": 1,\n\t\t\t\"maxItems\": 100,\n\t\t\t\"uniqueItems\": true,\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"pattern\": \"^[^&| @!#%^();:/\\\\\\\\}{=+?<>,~`'\\\"]+$\"\n\t\t\t},\n\t\t\t\"example\": [\"example.com\", \"www.example.com\"]\n\t\t},\n\t\t\"enabled\": {\n\t\t\t\"description\": \"Is Enabled\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": false\n\t\t},\n\t\t\"ssl_forced\": {\n\t\t\t\"description\": \"Is SSL Forced\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"hsts_enabled\": {\n\t\t\t\"description\": \"Is HSTS Enabled\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"hsts_subdomains\": {\n\t\t\t\"description\": \"Is HSTS applicable to all subdomains\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"ssl_provider\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^(letsencrypt|other)$\",\n\t\t\t\"example\": \"letsencrypt\"\n\t\t},\n\t\t\"http2_support\": {\n\t\t\t\"description\": \"HTTP2 Protocol Support\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"block_exploits\": {\n\t\t\t\"description\": \"Should we block common exploits\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": false\n\t\t},\n\t\t\"caching_enabled\": {\n\t\t\t\"description\": \"Should we cache assets\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"email\": {\n\t\t\t\"description\": \"Email address\",\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\\.[A-Za-z]{2,}$\",\n\t\t\t\"example\": \"me@example.com\"\n\t\t},\n\t\t\"directive\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"enum\": [\"allow\", \"deny\"],\n\t\t\t\"example\": \"allow\"\n\t\t},\n\t\t\"address\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"pattern\": \"^([0-9]{1,3}\\\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"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]))?$\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"pattern\": \"^all$\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": \"192.168.0.11\"\n\t\t},\n\t\t\"access_items\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"username\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"minLength\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"password\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\t\"password\": \"pass\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"example\": [\n\t\t\t\t{\n\t\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\t\"password\": \"pass\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"access_clients\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"address\": {\n\t\t\t\t\t\t\"$ref\": \"#/properties/address\"\n\t\t\t\t\t},\n\t\t\t\t\t\"directive\": {\n\t\t\t\t\t\t\"$ref\": \"#/properties/directive\"\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\"address\": \"192.168.0.0/24\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"example\": [\n\t\t\t\t{\n\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\"address\": \"192.168.0.0/24\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"certificate_files\": {\n\t\t\t\"description\": \"Certificate Files\",\n\t\t\t\"content\": {\n\t\t\t\t\"multipart/form-data\": {\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"required\": [\"certificate\", \"certificate_key\"],\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"certificate\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"certificate_key\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"intermediate_certificate\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"example\": {\n\t\t\t\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\",\n\t\t\t\t\t\t\"certificate_key\": \"-----BEGIN PRIVATE\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/access-list-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Access List object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"owner_user_id\", \"name\", \"meta\", \"satisfy_any\", \"pass_auth\", \"proxy_host_count\"],\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"name\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"example\": \"My Access List\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {}\n\t\t},\n\t\t\"satisfy_any\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"pass_auth\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": false\n\t\t},\n\t\t\"proxy_host_count\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 0,\n\t\t\t\"example\": 3\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/audit-log-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Audit Log list\",\n\t\"items\": {\n\t\t\"$ref\": \"./audit-log-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/audit-log-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Audit Log object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"user_id\",\n\t\t\"object_type\",\n\t\t\"object_id\",\n\t\t\"action\",\n\t\t\"meta\"\n\t],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"object_type\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"certificate\"\n\t\t},\n\t\t\"object_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"action\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"created\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {}\n\t\t},\n\t\t\"user\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/certificate-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Certificates list\",\n\t\"items\": {\n\t\t\"$ref\": \"./certificate-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/certificate-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Certificate object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"owner_user_id\", \"provider\", \"nice_name\", \"domain_names\", \"expires_on\", \"meta\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"provider\": {\n\t\t\t\"$ref\": \"../common.json#/properties/ssl_provider\"\n\t\t},\n\t\t\"nice_name\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Nice Name for the custom certificate\",\n\t\t\t\"example\": \"My Custom Cert\"\n\t\t},\n\t\t\"domain_names\": {\n\t\t\t\"description\": \"Domain Names separated by a comma\",\n\t\t\t\"type\": \"array\",\n\t\t\t\"maxItems\": 100,\n\t\t\t\"uniqueItems\": true,\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"pattern\": \"^[^&| @!#%^();:/\\\\\\\\}{=+?<>,~`'\\\"]+$\"\n\t\t\t},\n\t\t\t\"example\": [\"example.com\", \"www.example.com\"]\n\t\t},\n\t\t\"expires_on\": {\n\t\t\t\"description\": \"Date and time of expiration\",\n\t\t\t\"readOnly\": true,\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"2025-10-28T04:17:54.000Z\"\n\t\t},\n\t\t\"owner\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"additionalProperties\": false,\n\t\t\t\"properties\": {\n\t\t\t\t\"certificate\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"minLength\": 1\n\t\t\t\t},\n\t\t\t\t\"certificate_key\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"minLength\": 1\n\t\t\t\t},\n\t\t\t\t\"dns_challenge\": {\n\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t},\n\t\t\t\t\"dns_provider_credentials\": {\n\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t},\n\t\t\t\t\"dns_provider\": {\n\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t},\n\t\t\t\t\"letsencrypt_certificate\": {\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t\"propagation_seconds\": {\n\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\"minimum\": 0\n\t\t\t\t},\n\t\t\t\t\"key_type\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"enum\": [\"rsa\", \"ecdsa\"],\n\t\t\t\t\t\"default\": \"rsa\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"example\": {\n\t\t\t\t\"dns_challenge\": false\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/check-version-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Check Version object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"current\", \"latest\", \"update_available\"],\n\t\"properties\": {\n\t\t\"current\": {\n\t\t\t\"type\": [\"string\", \"null\"],\n\t\t\t\"description\": \"Current version string\",\n\t\t\t\"example\": \"v2.10.1\"\n\t\t},\n\t\t\"latest\": {\n\t\t\t\"type\": [\"string\", \"null\"],\n\t\t\t\"description\": \"Latest version string\",\n\t\t\t\"example\": \"v2.13.4\"\n\t\t},\n\t\t\"update_available\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"description\": \"Whether there's an update available\",\n\t\t\t\"example\": true\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/dead-host-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"404 Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./dead-host-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/dead-host-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"404 Host object\",\n\t\"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\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"domain_names\": {\n\t\t\t\"$ref\": \"../common.json#/properties/domain_names\"\n\t\t},\n\t\t\"certificate_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/certificate_id\"\n\t\t},\n\t\t\"ssl_forced\": {\n\t\t\t\"$ref\": \"../common.json#/properties/ssl_forced\"\n\t\t},\n\t\t\"hsts_enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_enabled\"\n\t\t},\n\t\t\"hsts_subdomains\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_subdomains\"\n\t\t},\n\t\t\"http2_support\": {\n\t\t\t\"$ref\": \"../common.json#/properties/http2_support\"\n\t\t},\n\t\t\"advanced_config\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"\"\n\t\t},\n\t\t\"enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/enabled\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {}\n\t\t},\n\t\t\"certificate\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\",\n\t\t\t\t\t\"example\": null\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"$ref\": \"./certificate-object.json\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": null\n\t\t},\n\t\t\"owner\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/dns-providers-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"DNS Providers list\",\n\t\"items\": {\n\t\t\"type\": \"object\",\n\t\t\"required\": [\"id\", \"name\", \"credentials\"],\n\t\t\"additionalProperties\": false,\n\t\t\"properties\": {\n\t\t\t\"id\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"description\": \"Unique identifier for the DNS provider, matching the python package\"\n\t\t\t},\n\t\t\t\"name\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"description\": \"Human-readable name of the DNS provider\"\n\t\t\t},\n\t\t\t\"credentials\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"description\": \"Instructions on how to format the credentials for this DNS provider\"\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/error-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Error object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"code\", \"message\"],\n\t\"properties\": {\n\t\t\"code\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"example\": 400\n\t\t},\n\t\t\"message\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"Bad Request\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/error.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Error\",\n\t\"properties\": {\n\t\t\"error\": {\n\t\t\t\"$ref\": \"./error-object.json\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/health-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Health object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"status\", \"version\"],\n\t\"properties\": {\n\t\t\"status\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Healthy\",\n\t\t\t\"example\": \"OK\"\n\t\t},\n\t\t\"setup\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"description\": \"Whether the initial setup has been completed\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"version\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"description\": \"The version object\",\n\t\t\t\"example\": {\n\t\t\t\t\"major\": 2,\n\t\t\t\t\"minor\": 0,\n\t\t\t\t\"revision\": 0\n\t\t\t},\n\t\t\t\"additionalProperties\": false,\n\t\t\t\"required\": [\"major\", \"minor\", \"revision\"],\n\t\t\t\"properties\": {\n\t\t\t\t\"major\": {\n\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\"minimum\": 0,\n\t\t\t\t\t\"example\": 2\n\t\t\t\t},\n\t\t\t\t\"minor\": {\n\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\"minimum\": 0,\n\t\t\t\t\t\"example\": 10\n\t\t\t\t},\n\t\t\t\t\"revision\": {\n\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\"minimum\": 0,\n\t\t\t\t\t\"example\": 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/permission-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"minProperties\": 1,\n\t\"properties\": {\n\t\t\"visibility\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Visibility Type\",\n\t\t\t\"enum\": [\"all\", \"user\"],\n\t\t\t\"example\": \"all\"\n\t\t},\n\t\t\"access_lists\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Access Lists Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"view\"\n\t\t},\n\t\t\"dead_hosts\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"404 Hosts Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"manage\"\n\t\t},\n\t\t\"proxy_hosts\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Proxy Hosts Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"hidden\"\n\t\t},\n\t\t\"redirection_hosts\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Redirection Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"view\"\n\t\t},\n\t\t\"streams\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Streams Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"manage\"\n\t\t},\n\t\t\"certificates\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Certificates Permissions\",\n\t\t\t\"enum\": [\"hidden\", \"view\", \"manage\"],\n\t\t\t\"example\": \"hidden\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/proxy-host-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Proxy Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./proxy-host-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/proxy-host-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Proxy Host object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"owner_user_id\",\n\t\t\"domain_names\",\n\t\t\"forward_host\",\n\t\t\"forward_port\",\n\t\t\"access_list_id\",\n\t\t\"certificate_id\",\n\t\t\"ssl_forced\",\n\t\t\"caching_enabled\",\n\t\t\"block_exploits\",\n\t\t\"advanced_config\",\n\t\t\"meta\",\n\t\t\"allow_websocket_upgrade\",\n\t\t\"http2_support\",\n\t\t\"forward_scheme\",\n\t\t\"enabled\",\n\t\t\"locations\",\n\t\t\"hsts_enabled\",\n\t\t\"hsts_subdomains\",\n\t\t\"trust_forwarded_proto\"\n\t],\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"domain_names\": {\n\t\t\t\"$ref\": \"../common.json#/properties/domain_names\"\n\t\t},\n\t\t\"forward_host\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"maxLength\": 255,\n\t\t\t\"example\": \"127.0.0.1\"\n\t\t},\n\t\t\"forward_port\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"maximum\": 65535,\n\t\t\t\"example\": 8080\n\t\t},\n\t\t\"access_list_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/access_list_id\"\n\t\t},\n\t\t\"certificate_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/certificate_id\"\n\t\t},\n\t\t\"ssl_forced\": {\n\t\t\t\"$ref\": \"../common.json#/properties/ssl_forced\"\n\t\t},\n\t\t\"caching_enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/caching_enabled\"\n\t\t},\n\t\t\"block_exploits\": {\n\t\t\t\"$ref\": \"../common.json#/properties/block_exploits\"\n\t\t},\n\t\t\"advanced_config\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {\n\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\"nginx_err\": null\n\t\t\t}\n\t\t},\n\t\t\"allow_websocket_upgrade\": {\n\t\t\t\"description\": \"Allow Websocket Upgrade for all paths\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"http2_support\": {\n\t\t\t\"$ref\": \"../common.json#/properties/http2_support\"\n\t\t},\n\t\t\"forward_scheme\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"enum\": [\"http\", \"https\"],\n\t\t\t\"example\": \"http\"\n\t\t},\n\t\t\"enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/enabled\"\n\t\t},\n\t\t\"locations\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"minItems\": 0,\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"required\": [\"forward_scheme\", \"forward_host\", \"forward_port\", \"path\"],\n\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"id\": {\n\t\t\t\t\t\t\"type\": [\"integer\", \"null\"]\n\t\t\t\t\t},\n\t\t\t\t\t\"path\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"minLength\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"forward_scheme\": {\n\t\t\t\t\t\t\"$ref\": \"#/properties/forward_scheme\"\n\t\t\t\t\t},\n\t\t\t\t\t\"forward_host\": {\n\t\t\t\t\t\t\"$ref\": \"#/properties/forward_host\"\n\t\t\t\t\t},\n\t\t\t\t\t\"forward_port\": {\n\t\t\t\t\t\t\"$ref\": \"#/properties/forward_port\"\n\t\t\t\t\t},\n\t\t\t\t\t\"forward_path\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"example\": [\n\t\t\t\t{\n\t\t\t\t\t\"path\": \"/app\",\n\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\"forward_host\": \"example.com\",\n\t\t\t\t\t\"forward_port\": 80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hsts_enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_enabled\"\n\t\t},\n\t\t\"hsts_subdomains\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_subdomains\"\n\t\t},\n\t\t\"trust_forwarded_proto\":{\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"description\": \"Trust the forwarded headers\",\n\t\t\t\"example\": false\n\t\t},\n\t\t\"certificate\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\",\n\t\t\t\t\t\"example\": null\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"$ref\": \"./certificate-object.json\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": null\n\t\t},\n\t\t\"owner\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t},\n\t\t\"access_list\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\",\n\t\t\t\t\t\"example\": null\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"$ref\": \"./access-list-object.json\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": null\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/redirection-host-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Redirection Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./redirection-host-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/redirection-host-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Redirection Host object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"owner_user_id\",\n\t\t\"domain_names\",\n\t\t\"forward_http_code\",\n\t\t\"forward_scheme\",\n\t\t\"forward_domain_name\",\n\t\t\"preserve_path\",\n\t\t\"certificate_id\",\n\t\t\"ssl_forced\",\n\t\t\"hsts_enabled\",\n\t\t\"hsts_subdomains\",\n\t\t\"http2_support\",\n\t\t\"block_exploits\",\n\t\t\"advanced_config\",\n\t\t\"enabled\",\n\t\t\"meta\"\n\t],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"domain_names\": {\n\t\t\t\"$ref\": \"../common.json#/properties/domain_names\"\n\t\t},\n\t\t\"forward_http_code\": {\n\t\t\t\"description\": \"Redirect HTTP Status Code\",\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 300,\n\t\t\t\"maximum\": 308,\n\t\t\t\"example\": 302\n\t\t},\n\t\t\"forward_scheme\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"enum\": [\n\t\t\t\t\"auto\",\n\t\t\t\t\"http\",\n\t\t\t\t\"https\"\n\t\t\t],\n\t\t\t\"example\": \"http\"\n\t\t},\n\t\t\"forward_domain_name\": {\n\t\t\t\"description\": \"Domain Name\",\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^(?:[^.*]+\\\\.?)+[^.]$\",\n\t\t\t\"example\": \"jc21.com\"\n\t\t},\n\t\t\"preserve_path\": {\n\t\t\t\"description\": \"Should the path be preserved\",\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"certificate_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/certificate_id\"\n\t\t},\n\t\t\"ssl_forced\": {\n\t\t\t\"$ref\": \"../common.json#/properties/ssl_forced\"\n\t\t},\n\t\t\"hsts_enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_enabled\"\n\t\t},\n\t\t\"hsts_subdomains\": {\n\t\t\t\"$ref\": \"../common.json#/properties/hsts_subdomains\"\n\t\t},\n\t\t\"http2_support\": {\n\t\t\t\"$ref\": \"../common.json#/properties/http2_support\"\n\t\t},\n\t\t\"block_exploits\": {\n\t\t\t\"$ref\": \"../common.json#/properties/block_exploits\"\n\t\t},\n\t\t\"advanced_config\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"example\": \"\"\n\t\t},\n\t\t\"enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/enabled\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {\n\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\"nginx_err\": null\n\t\t\t}\n\t\t},\n\t\t\"certificate\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\",\n\t\t\t\t\t\"example\": null\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"$ref\": \"./certificate-object.json\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": null\n\t\t},\n\t\t\"owner\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/security-schemes.json",
    "content": "{\n\t\"bearerAuth\": {\n\t\t\"type\": \"http\",\n\t\t\"scheme\": \"bearer\",\n\t\t\"bearerFormat\": \"JWT\",\n\t\t\"description\": \"JWT Bearer Token authentication\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/setting-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Setting list\",\n\t\"items\": {\n\t\t\"$ref\": \"./setting-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/setting-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Setting object\",\n\t\"required\": [\"id\", \"name\", \"description\", \"value\", \"meta\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Setting ID\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"example\": \"default-site\"\n\t\t},\n\t\t\"name\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Setting Display Name\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"example\": \"Default Site\"\n\t\t},\n\t\t\"description\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Meaningful description\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"example\": \"What to show when Nginx is hit with an unknown Host\"\n\t\t},\n\t\t\"value\": {\n\t\t\t\"description\": \"Value in almost any form\",\n\t\t\t\"example\": \"congratulations\",\n\t\t\t\"anyOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"minLength\": 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"integer\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"array\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"description\": \"Extra metadata\",\n\t\t\t\"example\": {\n\t\t\t\t\"redirect\": \"http://example.com\",\n\t\t\t\t\"html\": \"<h1>404</h1>\"\n\t\t\t},\n\t\t\t\"type\": \"object\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/stream-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"Streams list\",\n\t\"items\": {\n\t\t\"$ref\": \"./stream-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/stream-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Stream object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"owner_user_id\",\n\t\t\"incoming_port\",\n\t\t\"forwarding_host\",\n\t\t\"forwarding_port\",\n\t\t\"tcp_forwarding\",\n\t\t\"udp_forwarding\",\n\t\t\"enabled\",\n\t\t\"meta\"\n\t],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/id\"\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/created_on\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"$ref\": \"../common.json#/properties/modified_on\"\n\t\t},\n\t\t\"owner_user_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/user_id\"\n\t\t},\n\t\t\"incoming_port\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"maximum\": 65535,\n\t\t\t\"example\": 9090\n\t\t},\n\t\t\"forwarding_host\": {\n\t\t\t\"anyOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"description\": \"Domain Name\",\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"pattern\": \"^(?:[^.*]+\\\\.?)+[^.]$\",\n\t\t\t\t\t\"example\": \"example.com\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"format\": \"^[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}$\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"format\": \"ipv6\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": \"example.com\"\n\t\t},\n\t\t\"forwarding_port\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"maximum\": 65535,\n\t\t\t\"example\": 80\n\t\t},\n\t\t\"tcp_forwarding\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"udp_forwarding\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"example\": false\n\t\t},\n\t\t\"enabled\": {\n\t\t\t\"$ref\": \"../common.json#/properties/enabled\"\n\t\t},\n\t\t\"certificate_id\": {\n\t\t\t\"$ref\": \"../common.json#/properties/certificate_id\"\n\t\t},\n\t\t\"meta\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"example\": {}\n\t\t},\n\t\t\"certificate\": {\n\t\t\t\"oneOf\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"null\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"$ref\": \"./certificate-object.json\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"example\": null\n\t\t},\n\t\t\"owner\": {\n\t\t\t\"$ref\": \"./user-object.json\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/token-challenge.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Token object\",\n\t\"required\": [\"requires_2fa\", \"challenge_token\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"requires_2fa\": {\n\t\t\t\"description\": \"Whether this token request requires two-factor authentication\",\n\t\t\t\"example\": true,\n\t\t\t\"type\": \"boolean\"\n\t\t},\n\t\t\"challenge_token\": {\n\t\t\t\"description\": \"Challenge Token used in subsequent 2FA verification\",\n\t\t\t\"example\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\",\n\t\t\t\"type\": \"string\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/token-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"Token object\",\n\t\"required\": [\"expires\", \"token\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"expires\": {\n\t\t\t\"description\": \"Token Expiry ISO Time String\",\n\t\t\t\"example\": \"2025-02-04T20:40:46.340Z\",\n\t\t\t\"type\": \"string\"\n\t\t},\n\t\t\"token\": {\n\t\t\t\"description\": \"JWT Token\",\n\t\t\t\"example\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\",\n\t\t\t\"type\": \"string\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/user-list.json",
    "content": "{\n\t\"type\": \"array\",\n\t\"description\": \"User list\",\n\t\"items\": {\n\t\t\"$ref\": \"./user-object.json\"\n\t}\n}\n"
  },
  {
    "path": "backend/schema/components/user-object.json",
    "content": "{\n\t\"type\": \"object\",\n\t\"description\": \"User object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"is_disabled\", \"email\", \"name\", \"nickname\", \"avatar\", \"roles\"],\n\t\"additionalProperties\": false,\n\t\"properties\": {\n\t\t\"id\": {\n\t\t\t\"type\": \"integer\",\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"minimum\": 1,\n\t\t\t\"example\": 1\n\t\t},\n\t\t\"created_on\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Created Date\",\n\t\t\t\"example\": \"2020-01-30T09:36:08.000Z\"\n\t\t},\n\t\t\"modified_on\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Modified Date\",\n\t\t\t\"example\": \"2020-01-30T09:41:04.000Z\"\n\t\t},\n\t\t\"is_disabled\": {\n\t\t\t\"type\": \"boolean\",\n\t\t\t\"description\": \"Is user Disabled\",\n\t\t\t\"example\": true\n\t\t},\n\t\t\"email\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Email\",\n\t\t\t\"minLength\": 3,\n\t\t\t\"example\": \"jc@jc21.com\"\n\t\t},\n\t\t\"name\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Name\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"example\": \"Jamie Curnow\"\n\t\t},\n\t\t\"nickname\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Nickname\",\n\t\t\t\"example\": \"James\"\n\t\t},\n\t\t\"avatar\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Gravatar URL based on email, without scheme\",\n\t\t\t\"example\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\"\n\t\t},\n\t\t\"roles\": {\n\t\t\t\"description\": \"Roles applied\",\n\t\t\t\"example\": [\"admin\"],\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t},\n\t\t\"permissions\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"description\": \"Permissions if expanded in request\",\n\t\t\t\"required\": [\n\t\t\t\t\"visibility\",\n\t\t\t\t\"proxy_hosts\",\n\t\t\t\t\"redirection_hosts\",\n\t\t\t\t\"dead_hosts\",\n\t\t\t\t\"streams\",\n\t\t\t\t\"access_lists\",\n\t\t\t\t\"certificates\"\n\t\t\t],\n\t\t\t\"properties\": {\n\t\t\t\t\"visibility\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Visibility level\",\n\t\t\t\t\t\"example\": \"all\",\n\t\t\t\t\t\"pattern\": \"^(all|user)$\"\n\t\t\t\t},\n\t\t\t\t\"proxy_hosts\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Proxy Hosts access level\",\n\t\t\t\t\t\"example\": \"manage\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t},\n\t\t\t\t\"redirection_hosts\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Redirection Hosts access level\",\n\t\t\t\t\t\"example\": \"manage\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t},\n\t\t\t\t\"dead_hosts\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Dead Hosts access level\",\n\t\t\t\t\t\"example\": \"manage\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t},\n\t\t\t\t\"streams\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Streams access level\",\n\t\t\t\t\t\"example\": \"manage\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t},\n\t\t\t\t\"access_lists\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Access Lists access level\",\n\t\t\t\t\t\"example\": \"hidden\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t},\n\t\t\t\t\"certificates\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"description\": \"Certificates access level\",\n\t\t\t\t\t\"example\": \"view\",\n\t\t\t\t\t\"pattern\": \"^(manage|view|hidden)$\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/index.js",
    "content": "import { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport $RefParser from \"@apidevtools/json-schema-ref-parser\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nlet compiledSchema = null;\n\n/**\n * Compiles the schema, by dereferencing it, only once\n * and returns the memory cached value\n */\nconst getCompiledSchema = async () => {\n\tif (compiledSchema === null) {\n\t\tcompiledSchema = await $RefParser.dereference(`${__dirname}/swagger.json`, {\n\t\t\tmutateInputSchema: false,\n\t\t});\n\t}\n\treturn compiledSchema;\n};\n\n/**\n * Scans the schema for the validation schema for the given path and method\n * and returns it.\n *\n * @param {string} path\n * @param {string} method\n * @returns string|null\n */\nconst getValidationSchema = (path, method) => {\n\tif (\n\t\tcompiledSchema !== null &&\n\t\ttypeof compiledSchema.paths[path] !== \"undefined\" &&\n\t\ttypeof compiledSchema.paths[path][method] !== \"undefined\" &&\n\t\ttypeof compiledSchema.paths[path][method].requestBody !== \"undefined\" &&\n\t\ttypeof compiledSchema.paths[path][method].requestBody.content !== \"undefined\" &&\n\t\ttypeof compiledSchema.paths[path][method].requestBody.content[\"application/json\"] !== \"undefined\" &&\n\t\ttypeof compiledSchema.paths[path][method].requestBody.content[\"application/json\"].schema !== \"undefined\"\n\t) {\n\t\treturn compiledSchema.paths[path][method].requestBody.content[\"application/json\"].schema;\n\t}\n\treturn null;\n};\n\nexport { getCompiledSchema, getValidationSchema };\n"
  },
  {
    "path": "backend/schema/paths/audit-log/get.json",
    "content": "{\n\t\"operationId\": \"getAuditLogs\",\n\t\"summary\": \"Get Audit Logs\",\n\t\"tags\": [\"audit-log\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 7,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T13:09:54.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T13:09:54.000Z\",\n\t\t\t\t\t\t\t\t\t\"user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"object_type\": \"user\",\n\t\t\t\t\t\t\t\t\t\"object_id\": 3,\n\t\t\t\t\t\t\t\t\t\"action\": \"updated\",\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"name\": \"John Doe\",\n\t\t\t\t\t\t\t\t\t\t\"permissions\": {\n\t\t\t\t\t\t\t\t\t\t\t\"user_id\": 3,\n\t\t\t\t\t\t\t\t\t\t\t\"visibility\": \"all\",\n\t\t\t\t\t\t\t\t\t\t\t\"access_lists\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\t\"dead_hosts\": \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\t\"proxy_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\t\"redirection_hosts\": \"view\",\n\t\t\t\t\t\t\t\t\t\t\t\"streams\": \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\t\"certificates\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\t\"id\": 3,\n\t\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T13:09:54.000Z\",\n\t\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T13:09:51.000Z\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../components/audit-log-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/audit-log/id/get.json",
    "content": "{\n\t\"operationId\": \"getAuditLog\",\n\t\"summary\": \"Get Audit Log Event\",\n\t\"tags\": [\"audit-log\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"admin\"\n\t\t\t]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"id\",\n\t\t\t\"description\": \"Audit Log Event ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2025-09-15T17:27:45.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2025-09-15T17:27:45.000Z\",\n\t\t\t\t\t\t\t\t\"user_id\": 1,\n\t\t\t\t\t\t\t\t\"object_type\": \"user\",\n\t\t\t\t\t\t\t\t\"object_id\": 1,\n\t\t\t\t\t\t\t\t\"action\": \"created\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-09-15T17:27:45.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-09-15T17:27:45.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Jamie\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Jamie\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"permissions\": {\n\t\t\t\t\t\t\t\t\t\t\"visibility\": \"all\",\n\t\t\t\t\t\t\t\t\t\t\"proxy_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"redirection_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"dead_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"streams\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"access_lists\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"certificates\": \"manage\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/audit-log-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/get.json",
    "content": "{\n\t\"operationId\": \"health\",\n\t\"summary\": \"Returns the API health status\",\n\t\"tags\": [\"public\"],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"status\": \"OK\",\n\t\t\t\t\t\t\t\t\"setup\": true,\n\t\t\t\t\t\t\t\t\"version\": {\n\t\t\t\t\t\t\t\t\t\"major\": 2,\n\t\t\t\t\t\t\t\t\t\"minor\": 1,\n\t\t\t\t\t\t\t\t\t\"revision\": 0\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../components/health-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/access-lists/get.json",
    "content": "{\n\t\"operationId\": \"getAccessLists\",\n\t\"summary\": \"Get all access lists\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"access_lists.view\"\n\t\t\t]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\n\t\t\t\t\t\"owner\",\n\t\t\t\t\t\"items\",\n\t\t\t\t\t\"clients\",\n\t\t\t\t\t\"proxy_hosts\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"example\": {\n\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\"name\": \"test1234\",\n\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\"satisfy_any\": true,\n\t\t\t\t\t\t\"pass_auth\": false,\n\t\t\t\t\t\t\"proxy_host_count\": 0\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/access-list-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/access-lists/listID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteAccessList\",\n\t\"summary\": \"Delete a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"access_lists.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"listID\",\n\t\t\t\"description\": \"Access List ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/access-lists/listID/get.json",
    "content": "{\n    \"operationId\": \"getAccessList\",\n    \"summary\": \"Get a access List\",\n    \"tags\": [\n        \"access-lists\"\n    ],\n    \"security\": [\n        {\n            \"bearerAuth\": [\n                \"access_lists.view\"\n            ]\n        }\n    ],\n    \"parameters\": [\n        {\n            \"in\": \"path\",\n            \"name\": \"listID\",\n            \"description\": \"Access List ID\",\n            \"schema\": {\n                \"type\": \"integer\",\n                \"minimum\": 1\n            },\n            \"required\": true,\n            \"example\": 1\n        }\n    ],\n    \"responses\": {\n        \"200\": {\n            \"description\": \"200 response\",\n            \"content\": {\n                \"application/json\": {\n                    \"examples\": {\n                        \"default\": {\n                            \"value\": {\n                                \"id\": 1,\n                                \"created_on\": \"2025-10-28T04:06:55.000Z\",\n                                \"modified_on\": \"2025-10-29T22:48:20.000Z\",\n                                \"owner_user_id\": 1,\n                                \"name\": \"My Access List\",\n                                \"meta\": {},\n                                \"satisfy_any\": false,\n                                \"pass_auth\": false,\n                                \"proxy_host_count\": 1\n                            }\n                        }\n                    },\n                    \"schema\": {\n                        \"$ref\": \"../../../../components/access-list-object.json\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/access-lists/listID/put.json",
    "content": "{\n\t\"operationId\": \"updateAccessList\",\n\t\"summary\": \"Update a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"access_lists.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"listID\",\n\t\t\t\"description\": \"Access List ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Access List Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/access-list-object.json#/properties/name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"satisfy_any\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/access-list-object.json#/properties/satisfy_any\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"pass_auth\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/access-list-object.json#/properties/pass_auth\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../common.json#/properties/access_items\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"clients\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../common.json#/properties/access_clients\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"name\": \"My Access List\",\n\t\t\t\t\t\"satisfy_any\": true,\n\t\t\t\t\t\"pass_auth\": false,\n\t\t\t\t\t\"items\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"username\": \"admin2\",\n\t\t\t\t\t\t\t\"password\": \"pass2\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"clients\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\t\t\"address\": \"192.168.0.0/24\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:34:34.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"name\": \"test123!!\",\n\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\"satisfy_any\": true,\n\t\t\t\t\t\t\t\t\"pass_auth\": false,\n\t\t\t\t\t\t\t\t\"proxy_host_count\": 0,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-07T22:43:55.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T12:52:54.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"some guy\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"items\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\t\t\t\t\t\t\"password\": \"\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\t\t\"hint\": \"a****\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"username\": \"asdad\",\n\t\t\t\t\t\t\t\t\t\t\"password\": \"\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\t\t\"hint\": \"a*****\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"clients\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"address\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"proxy_hosts\": []\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/access-list-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/access-lists/post.json",
    "content": "{\n\t\"operationId\": \"createAccessList\",\n\t\"summary\": \"Create a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"access_lists.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Access List Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\n\t\t\t\t\t\t\"name\"\n\t\t\t\t\t],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/access-list-object.json#/properties/name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"satisfy_any\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/access-list-object.json#/properties/satisfy_any\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"pass_auth\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/access-list-object.json#/properties/pass_auth\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../common.json#/properties/access_items\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"clients\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../common.json#/properties/access_clients\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"name\": \"My Access List\",\n\t\t\t\t\t\"satisfy_any\": true,\n\t\t\t\t\t\"pass_auth\": false,\n\t\t\t\t\t\"items\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\t\t\t\"password\": \"pass\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"clients\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\t\t\"address\": \"192.168.0.0/24\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"name\": \"test1234\",\n\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\"satisfy_any\": true,\n\t\t\t\t\t\t\t\t\"pass_auth\": false,\n\t\t\t\t\t\t\t\t\"proxy_host_count\": 0,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-07T22:43:55.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T12:52:54.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"some guy\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"items\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\t\t\t\t\t\t\"password\": \"\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\t\t\"hint\": \"a****\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"username\": \"asdad\",\n\t\t\t\t\t\t\t\t\t\t\"password\": \"\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\t\t\"hint\": \"a*****\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"proxy_hosts\": [],\n\t\t\t\t\t\t\t\t\"clients\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-08T22:15:40.000Z\",\n\t\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\t\"address\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\t\t\"directive\": \"allow\",\n\t\t\t\t\t\t\t\t\t\t\"meta\": {}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/access-list-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/certID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteCertificate\",\n\t\"summary\": \"Delete a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"certID\",\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/certID/download/get.json",
    "content": "{\n\t\"operationId\": \"downloadCertificate\",\n\t\"summary\": \"Downloads a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"certID\",\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/zip\": {\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"format\": \"binary\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/certID/get.json",
    "content": "{\n\t\"operationId\": \"getCertificate\",\n\t\"summary\": \"Get a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"certID\",\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 4,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T05:31:58.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T05:32:11.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"provider\": \"letsencrypt\",\n\t\t\t\t\t\t\t\t\"nice_name\": \"test.example.com\",\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"expires_on\": \"2025-01-07T04:34:18.000Z\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"dns_challenge\": false\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/certificate-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/certID/renew/post.json",
    "content": "{\n\t\"operationId\": \"renewCertificate\",\n\t\"summary\": \"Renews a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"certID\",\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"expires_on\": \"2025-01-07T06:41:58.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T07:39:51.000Z\",\n\t\t\t\t\t\t\t\t\"id\": 4,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T05:31:58.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"provider\": \"letsencrypt\",\n\t\t\t\t\t\t\t\t\"nice_name\": \"My Test Cert\",\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.jc21.supernerd.pro\"],\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"dns_challenge\": false\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/certificate-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/certID/upload/post.json",
    "content": "{\n\t\"operationId\": \"uploadCertificate\",\n\t\"summary\": \"Uploads a custom Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"certID\",\n\t\t\t\"description\": \"Certificate ID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"$ref\": \"../../../../../common.json#/properties/certificate_files\"\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"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\",\n\t\t\t\t\t\t\t\t\"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\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"required\": [\"certificate\", \"certificate_key\"],\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"certificate\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"certificate_key\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"intermediate_certificate\": {\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\t\"example\": \"-----BEGIN CERTIFICATE-----\\nMIID...-----END CERTIFICATE-----\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/dns-providers/get.json",
    "content": "{\n\t\"operationId\": \"getDNSProviders\",\n\t\"summary\": \"Get DNS Providers for Certificates\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.view\"]\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": \"vultr\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Vultr\",\n\t\t\t\t\t\t\t\t\t\"credentials\": \"dns_vultr_key = YOUR_VULTR_API_KEY\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": \"websupport\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Websupport.sk\",\n\t\t\t\t\t\t\t\t\t\"credentials\": \"dns_websupport_identifier = <api_key>\\ndns_websupport_secret_key = <secret>\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": \"wedos\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Wedos\",\n\t\t\t\t\t\t\t\t\t\"credentials\": \"dns_wedos_user = <wedos_registration>\\ndns_wedos_auth = <wapi_password>\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": \"zoneedit\",\n\t\t\t\t\t\t\t\t\t\"name\": \"ZoneEdit\",\n\t\t\t\t\t\t\t\t\t\"credentials\": \"dns_zoneedit_user = <login-user-id>\\ndns_zoneedit_token = <dyn-authentication-token>\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/dns-providers-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/get.json",
    "content": "{\n\t\"operationId\": \"getCertificates\",\n\t\"summary\": \"Get all certificates\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\"owner\"]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 4,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T05:31:58.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T05:32:11.000Z\",\n\t\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"provider\": \"letsencrypt\",\n\t\t\t\t\t\t\t\t\t\"nice_name\": \"test.example.com\",\n\t\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\t\"expires_on\": \"2025-01-07T04:34:18.000Z\",\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"dns_challenge\": false\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/post.json",
    "content": "{\n\t\"operationId\": \"createCertificate\",\n\t\"summary\": \"Create a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Certificate Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\"provider\"],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"provider\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-object.json#/properties/provider\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"nice_name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-object.json#/properties/nice_name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"provider\": \"letsencrypt\",\n\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\"dns_challenge\": false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"expires_on\": \"2025-01-07 04:30:17\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09 05:28:51\",\n\t\t\t\t\t\t\t\t\"id\": 5,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09 05:28:35\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"provider\": \"letsencrypt\",\n\t\t\t\t\t\t\t\t\"nice_name\": \"test.example.com\",\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"dns_challenge\": false,\n\t\t\t\t\t\t\t\t\t\"letsencrypt_certificate\": {\n\t\t\t\t\t\t\t\t\t\t\"cn\": \"test.example.com\",\n\t\t\t\t\t\t\t\t\t\t\"issuer\": \"C = US, O = Let's Encrypt, CN = E5\",\n\t\t\t\t\t\t\t\t\t\t\"dates\": {\n\t\t\t\t\t\t\t\t\t\t\t\"from\": 1728448218,\n\t\t\t\t\t\t\t\t\t\t\t\"to\": 1736224217\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/certificate-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Domains are invalid\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/test-http/post.json",
    "content": "{\n\t\"operationId\": \"testHttpReach\",\n\t\"summary\": \"Test HTTP Reachability\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.view\"]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Test Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\"domains\"],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../common.json#/properties/domain_names\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"test.example.org\": \"ok\",\n\t\t\t\t\t\t\t\t\"test.example.com\": \"other:Invalid domain or IP\",\n\t\t\t\t\t\t\t\t\"nonexistent.example.com\": \"404\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/certificates/validate/post.json",
    "content": "{\n\t\"operationId\": \"validateCertificates\",\n\t\"summary\": \"Validates given Custom Certificates\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"certificates.manage\"]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"$ref\": \"../../../../common.json#/properties/certificate_files\"\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"certificate\": {\n\t\t\t\t\t\t\t\t\t\"cn\": \"mkcert\",\n\t\t\t\t\t\t\t\t\t\"issuer\": \"O = mkcert development CA, OU = jc@jc-Laptop.local (John Doe), CN = mkcert jc@jc-Laptop.local (John Doe)\",\n\t\t\t\t\t\t\t\t\t\"dates\": {\n\t\t\t\t\t\t\t\t\t\t\"from\": 1728458537,\n\t\t\t\t\t\t\t\t\t\t\"to\": 1799479337\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate_key\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"required\": [\"certificate\", \"certificate_key\"],\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"certificate\": {\n\t\t\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\t\t\"required\": [\"cn\", \"issuer\", \"dates\"],\n\t\t\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\t\t\"cn\": {\n\t\t\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\t\t\"example\": \"example.com\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"issuer\": {\n\t\t\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\t\t\"example\": \"C = US, O = Let's Encrypt, CN = E5\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"dates\": {\n\t\t\t\t\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\t\t\t\t\"required\": [\"from\", \"to\"],\n\t\t\t\t\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\t\t\t\t\"from\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"integer\"\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"to\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"type\": \"integer\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"example\": {\n\t\t\t\t\t\t\t\t\t\t\t\"from\": 1728448218,\n\t\t\t\t\t\t\t\t\t\t\t\"to\": 1736224217\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"certificate_key\": {\n\t\t\t\t\t\t\t\t\"type\": \"boolean\",\n\t\t\t\t\t\t\t\t\"example\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Certificate is not valid\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/get.json",
    "content": "{\n\t\"operationId\": \"getDeadHosts\",\n\t\"summary\": \"Get all 404 hosts\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\"owner\", \"certificate\"]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\t\"hsts_subdomains\": false\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/hostID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteDeadHost\",\n\t\"summary\": \"Delete a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the 404 Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json",
    "content": "{\n\t\"operationId\": \"disableDeadHost\",\n\t\"summary\": \"Disable a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the 404 Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already disabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json",
    "content": "{\n\t\"operationId\": \"enableDeadHost\",\n\t\"summary\": \"Enable a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the 404 Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already enabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/hostID/get.json",
    "content": "{\n\t\"operationId\": \"getDeadHost\",\n\t\"summary\": \"Get a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the 404 Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/hostID/put.json",
    "content": "{\n\t\"operationId\": \"updateDeadHost\",\n\t\"summary\": \"Update a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"dead_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the 404 Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"404 Host Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:46:06.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Admin\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate\": null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/dead-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/dead-hosts/post.json",
    "content": "{\n\t\"operationId\": \"create404Host\",\n\t\"summary\": \"Create a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"dead_hosts.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"404 Host Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\n\t\t\t\t\t\t\"domain_names\"\n\t\t\t\t\t],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t],\n\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\"meta\": {}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:38:52.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"certificate\": null,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Admin\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/get.json",
    "content": "{\n\t\"operationId\": \"getProxyHosts\",\n\t\"summary\": \"Get all proxy hosts\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"proxy_hosts.view\"\n\t\t\t]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\n\t\t\t\t\t\"access_list\",\n\t\t\t\t\t\"owner\",\n\t\t\t\t\t\"certificate\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-28T01:10:26.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-28T04:07:16.000Z\",\n\t\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\t\"test.jc21com\"\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\"forward_host\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\t\"forward_port\": 8081,\n\t\t\t\t\t\t\t\t\t\"access_list_id\": 1,\n\t\t\t\t\t\t\t\t\t\"certificate_id\": 1,\n\t\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\t\"caching_enabled\": false,\n\t\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"allow_websocket_upgrade\": false,\n\t\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\t\"locations\": [],\n\t\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\t\"trust_forwarded_proto\": false\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/hostID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteProxyHost\",\n\t\"summary\": \"Delete a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"proxy_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Proxy Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json",
    "content": "{\n\t\"operationId\": \"disableProxyHost\",\n\t\"summary\": \"Disable a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"proxy_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Proxy Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already disabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json",
    "content": "{\n\t\"operationId\": \"enableProxyHost\",\n\t\"summary\": \"Enable a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"proxy_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Proxy Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already enabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/hostID/get.json",
    "content": "{\n\t\"operationId\": \"getProxyHost\",\n\t\"summary\": \"Get a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"proxy_hosts.view\"\n\t\t\t]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Proxy Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 3,\n\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-30T01:12:05.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-30T01:12:05.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"forward_host\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\"forward_port\": 8080,\n\t\t\t\t\t\t\t\t\"access_list_id\": 0,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"caching_enabled\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"allow_websocket_upgrade\": false,\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"locations\": [],\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"trust_forwarded_proto\": false,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/hostID/put.json",
    "content": "{\n\t\"operationId\": \"updateProxyHost\",\n\t\"summary\": \"Update a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"proxy_hosts.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Proxy Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Proxy Host Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_scheme\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/forward_scheme\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_host\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/forward_host\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/forward_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"trust_forwarded_proto\": {\n    \t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"block_exploits\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/block_exploits\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"caching_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/caching_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"allow_websocket_upgrade\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"access_list_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/access_list_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/meta\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"locations\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json#/properties/locations\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 3,\n\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-30T01:12:05.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-30T01:17:06.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"forward_host\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\"forward_port\": 8080,\n\t\t\t\t\t\t\t\t\"access_list_id\": 0,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"caching_enabled\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"allow_websocket_upgrade\": false,\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"locations\": [],\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"trust_forwarded_proto\": false,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate\": null,\n\t\t\t\t\t\t\t\t\"access_list\": null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/proxy-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/proxy-hosts/post.json",
    "content": "{\n\t\"operationId\": \"createProxyHost\",\n\t\"summary\": \"Create a Proxy Host\",\n\t\"tags\": [\"proxy-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"proxy_hosts.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Proxy Host Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\n\t\t\t\t\t\t\"domain_names\",\n\t\t\t\t\t\t\"forward_scheme\",\n\t\t\t\t\t\t\"forward_host\",\n\t\t\t\t\t\t\"forward_port\"\n\t\t\t\t\t],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_scheme\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/forward_scheme\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_host\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/forward_host\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/forward_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"trust_forwarded_proto\": {\n    \t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/trust_forwarded_proto\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"block_exploits\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/block_exploits\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"caching_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/caching_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"allow_websocket_upgrade\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"access_list_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/access_list_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/meta\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"locations\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json#/properties/locations\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t],\n\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\"forward_host\": \"127.0.0.1\",\n\t\t\t\t\t\"forward_port\": 8080\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 3,\n\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-30T01:12:05.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-30T01:12:05.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"forward_host\": \"127.0.0.1\",\n\t\t\t\t\t\t\t\t\"forward_port\": 8080,\n\t\t\t\t\t\t\t\t\"access_list_id\": 0,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"caching_enabled\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\"allow_websocket_upgrade\": false,\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"locations\": [],\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"trust_forwarded_proto\": false,\n\t\t\t\t\t\t\t\t\"certificate\": null,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"access_list\": null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/proxy-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/get.json",
    "content": "{\n\t\"operationId\": \"getRedirectionHosts\",\n\t\"summary\": \"Get all Redirection hosts\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\"owner\", \"certificate\"]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:13:12.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:13:13.000Z\",\n\t\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\t\"forward_domain_name\": \"something-else.com\",\n\t\t\t\t\t\t\t\t\t\"preserve_path\": false,\n\t\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\t\"forward_http_code\": 301\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/hostID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteRedirectionHost\",\n\t\"summary\": \"Delete a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Redirection Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json",
    "content": "{\n\t\"operationId\": \"disableRedirectionHost\",\n\t\"summary\": \"Disable a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Redirection Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already disabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json",
    "content": "{\n\t\"operationId\": \"enableRedirectionHost\",\n\t\"summary\": \"Enable a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Redirection Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already enabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/hostID/get.json",
    "content": "{\n\t\"operationId\": \"getRedirectionHost\",\n\t\"summary\": \"Get a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Redirection Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:13:12.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:13:13.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"forward_domain_name\": \"something-else.com\",\n\t\t\t\t\t\t\t\t\"preserve_path\": false,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\"forward_http_code\": 301\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/hostID/put.json",
    "content": "{\n\t\"operationId\": \"updateRedirectionHost\",\n\t\"summary\": \"Update a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"redirection_hosts.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"hostID\",\n\t\t\t\"description\": \"The ID of the Redirection Host\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Redirection Host       Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_http_code\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/forward_http_code\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_scheme\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/forward_scheme\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_domain_name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/forward_domain_name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"preserve_path\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/preserve_path\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"block_exploits\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/block_exploits\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T01:13:12.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T01:18:11.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\"test.example.com\"],\n\t\t\t\t\t\t\t\t\"forward_domain_name\": \"something-else.com\",\n\t\t\t\t\t\t\t\t\"preserve_path\": false,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"http\",\n\t\t\t\t\t\t\t\t\"forward_http_code\": 301,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T00:59:56.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Admin\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate\": null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/redirection-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/redirection-hosts/post.json",
    "content": "{\n\t\"operationId\": \"createRedirectionHost\",\n\t\"summary\": \"Create a Redirection Host\",\n\t\"tags\": [\"redirection-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"redirection_hosts.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Redirection Host Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\n\t\t\t\t\t\t\"domain_names\",\n\t\t\t\t\t\t\"forward_scheme\",\n\t\t\t\t\t\t\"forward_http_code\",\n\t\t\t\t\t\t\"forward_domain_name\"\n\t\t\t\t\t],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_http_code\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/forward_http_code\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_scheme\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/forward_scheme\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forward_domain_name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/forward_domain_name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"preserve_path\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/preserve_path\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ssl_forced\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/ssl_forced\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_enabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/hsts_enabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"hsts_subdomains\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/hsts_subdomains\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"http2_support\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/http2_support\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"block_exploits\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/block_exploits\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"advanced_config\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/advanced_config\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t],\n\t\t\t\t\t\"forward_domain_name\": \"example.com\",\n\t\t\t\t\t\"forward_scheme\": \"auto\",\n\t\t\t\t\t\"forward_http_code\": 301,\n\t\t\t\t\t\"preserve_path\": false,\n\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\"meta\": {}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-30T01:27:04.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-30T01:27:04.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"domain_names\": [\n\t\t\t\t\t\t\t\t\t\"test.example.com\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"forward_domain_name\": \"example.com\",\n\t\t\t\t\t\t\t\t\"preserve_path\": false,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\t\t\t\"ssl_forced\": false,\n\t\t\t\t\t\t\t\t\"block_exploits\": false,\n\t\t\t\t\t\t\t\t\"advanced_config\": \"\",\n\t\t\t\t\t\t\t\t\"meta\": {},\n\t\t\t\t\t\t\t\t\"http2_support\": false,\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"hsts_enabled\": false,\n\t\t\t\t\t\t\t\t\"hsts_subdomains\": false,\n\t\t\t\t\t\t\t\t\"forward_scheme\": \"auto\",\n\t\t\t\t\t\t\t\t\"forward_http_code\": 301,\n\t\t\t\t\t\t\t\t\"certificate\": null,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2025-10-28T00:50:24.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"jamiec\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/redirection-host-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/get.json",
    "content": "{\n\t\"operationId\": \"getStreams\",\n\t\"summary\": \"Get all streams\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\"owner\", \"certificate\"]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\t\"incoming_port\": 9090,\n\t\t\t\t\t\t\t\t\t\"forwarding_host\": \"router.internal\",\n\t\t\t\t\t\t\t\t\t\"forwarding_port\": 80,\n\t\t\t\t\t\t\t\t\t\"tcp_forwarding\": true,\n\t\t\t\t\t\t\t\t\t\"udp_forwarding\": false,\n\t\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\t\"certificate_id\": 0\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/stream-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/post.json",
    "content": "{\n\t\"operationId\": \"createStream\",\n\t\"summary\": \"Create a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\n\t\t\t\t\"streams.manage\"\n\t\t\t]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Stream Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\n\t\t\t\t\t\t\"incoming_port\",\n\t\t\t\t\t\t\"forwarding_host\",\n\t\t\t\t\t\t\"forwarding_port\"\n\t\t\t\t\t],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"incoming_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/incoming_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forwarding_host\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/forwarding_host\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forwarding_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/forwarding_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"tcp_forwarding\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/tcp_forwarding\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"udp_forwarding\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/udp_forwarding\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json#/properties/meta\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"domain_names\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/dead-host-object.json#/properties/domain_names\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"incoming_port\": 8888,\n\t\t\t\t\t\"forwarding_host\": \"127.0.0.1\",\n\t\t\t\t\t\"forwarding_port\": 8080,\n\t\t\t\t\t\"tcp_forwarding\": true,\n\t\t\t\t\t\"udp_forwarding\": false,\n\t\t\t\t\t\"certificate_id\": 0,\n\t\t\t\t\t\"meta\": {}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"incoming_port\": 9090,\n\t\t\t\t\t\t\t\t\"forwarding_host\": \"router.internal\",\n\t\t\t\t\t\t\t\t\"forwarding_port\": 80,\n\t\t\t\t\t\t\t\t\"tcp_forwarding\": true,\n\t\t\t\t\t\t\t\t\"udp_forwarding\": false,\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:16.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:16.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Admin\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\n\t\t\t\t\t\t\t\t\t\t\"admin\"\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate_id\": 0\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/stream-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/streamID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteStream\",\n\t\"summary\": \"Delete a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"streamID\",\n\t\t\t\"description\": \"The ID of the Stream\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/streamID/disable/post.json",
    "content": "{\n\t\"operationId\": \"disableStream\",\n\t\"summary\": \"Disable a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"streamID\",\n\t\t\t\"description\": \"The ID of the Stream\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already disabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/streamID/enable/post.json",
    "content": "{\n\t\"operationId\": \"enableStream\",\n\t\"summary\": \"Enable a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"streamID\",\n\t\t\t\"description\": \"The ID of the Stream\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"400\": {\n\t\t\t\"description\": \"400 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"error\": {\n\t\t\t\t\t\t\t\t\t\"code\": 400,\n\t\t\t\t\t\t\t\t\t\"message\": \"Host is already enabled\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../../components/error.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/streamID/get.json",
    "content": "{\n\t\"operationId\": \"getStream\",\n\t\"summary\": \"Get a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.view\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"streamID\",\n\t\t\t\"description\": \"The ID of the Stream\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"incoming_port\": 9090,\n\t\t\t\t\t\t\t\t\"forwarding_host\": \"router.internal\",\n\t\t\t\t\t\t\t\t\"forwarding_port\": 80,\n\t\t\t\t\t\t\t\t\"tcp_forwarding\": true,\n\t\t\t\t\t\t\t\t\"udp_forwarding\": false,\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"certificate_id\": 0\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/nginx/streams/streamID/put.json",
    "content": "{\n\t\"operationId\": \"updateStream\",\n\t\"summary\": \"Update a Stream\",\n\t\"tags\": [\"streams\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"streams.manage\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"streamID\",\n\t\t\t\"description\": \"The ID of the Stream\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Stream Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"incoming_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/incoming_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forwarding_host\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/forwarding_host\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"forwarding_port\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/forwarding_port\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"tcp_forwarding\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/tcp_forwarding\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"udp_forwarding\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/udp_forwarding\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"certificate_id\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/certificate_id\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json#/properties/meta\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:45.000Z\",\n\t\t\t\t\t\t\t\t\"owner_user_id\": 1,\n\t\t\t\t\t\t\t\t\"incoming_port\": 9090,\n\t\t\t\t\t\t\t\t\"forwarding_host\": \"router.internal\",\n\t\t\t\t\t\t\t\t\"forwarding_port\": 80,\n\t\t\t\t\t\t\t\t\"tcp_forwarding\": true,\n\t\t\t\t\t\t\t\t\"udp_forwarding\": false,\n\t\t\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\t\t\"nginx_online\": true,\n\t\t\t\t\t\t\t\t\t\"nginx_err\": null\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\t\t\t\"owner\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2024-10-09T02:33:16.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2024-10-09T02:33:16.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"admin@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Administrator\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Admin\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"certificate_id\": 0\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../../components/stream-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/reports/hosts/get.json",
    "content": "{\n\t\"operationId\": \"reportsHosts\",\n\t\"summary\": \"Report on Host Statistics\",\n\t\"tags\": [\"reports\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": []\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"proxy\": 20,\n\t\t\t\t\t\t\t\t\"redirection\": 1,\n\t\t\t\t\t\t\t\t\"stream\": 0,\n\t\t\t\t\t\t\t\t\"dead\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"proxy\": {\n\t\t\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\t\t\"description\": \"Proxy Hosts Count\",\n\t\t\t\t\t\t\t\t\"example\": 20\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"redirection\": {\n\t\t\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\t\t\"description\": \"Redirection Hosts Count\",\n\t\t\t\t\t\t\t\t\"example\": 2\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"stream\": {\n\t\t\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\t\t\"description\": \"Streams Count\",\n\t\t\t\t\t\t\t\t\"example\": 0\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"dead\": {\n\t\t\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\t\t\"description\": \"404 Hosts Count\",\n\t\t\t\t\t\t\t\t\"example\": 3\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/schema/get.json",
    "content": "{\n\t\"operationId\": \"schema\",\n\t\"summary\": \"Returns this swagger API schema\",\n\t\"tags\": [\"public\"],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/settings/get.json",
    "content": "{\n\t\"operationId\": \"getSettings\",\n\t\"summary\": \"Get all settings\",\n\t\"tags\": [\"settings\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": \"default-site\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Default Site\",\n\t\t\t\t\t\t\t\t\t\"description\": \"What to show when Nginx is hit with an unknown Host\",\n\t\t\t\t\t\t\t\t\t\"value\": \"congratulations\",\n\t\t\t\t\t\t\t\t\t\"meta\": {}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../components/setting-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/settings/settingID/get.json",
    "content": "{\n\t\"operationId\": \"getSetting\",\n\t\"summary\": \"Get a setting\",\n\t\"tags\": [\"settings\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"settingID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"Setting ID\",\n\t\t\t\"example\": \"default-site\"\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": \"default-site\",\n\t\t\t\t\t\t\t\t\"name\": \"Default Site\",\n\t\t\t\t\t\t\t\t\"description\": \"What to show when Nginx is hit with an unknown Host\",\n\t\t\t\t\t\t\t\t\"value\": \"congratulations\",\n\t\t\t\t\t\t\t\t\"meta\": {}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/setting-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/settings/settingID/put.json",
    "content": "{\n\t\"operationId\": \"updateSetting\",\n\t\"summary\": \"Update a setting\",\n\t\"tags\": [\"settings\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"settingID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1,\n\t\t\t\t\"enum\": [\"default-site\"]\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"Setting ID\",\n\t\t\t\"example\": \"default-site\"\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Setting Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"enum\": [\"congratulations\", \"404\", \"444\", \"redirect\", \"html\"],\n\t\t\t\t\t\t\t\"example\": \"html\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"meta\": {\n\t\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\t\"redirect\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"html\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"example\": {\n\t\t\t\t\t\t\t\t\"html\": \"<p>hello world</p>\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"value\": \"congratulations\",\n\t\t\t\t\t\"meta\": {}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": \"default-site\",\n\t\t\t\t\t\t\t\t\"name\": \"Default Site\",\n\t\t\t\t\t\t\t\t\"description\": \"What to show when Nginx is hit with an unknown Host\",\n\t\t\t\t\t\t\t\t\"value\": \"congratulations\",\n\t\t\t\t\t\t\t\t\"meta\": {}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/setting-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/tokens/2fa/post.json",
    "content": "{\n\t\"operationId\": \"loginWith2FA\",\n\t\"summary\": \"Verify 2FA code and get full token\",\n\t\"tags\": [\"tokens\"],\n\t\"requestBody\": {\n\t\t\"description\": \"2fa Challenge Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"challenge_token\": {\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"code\": {\n\t\t\t\t\t\t\t\"minLength\": 6,\n\t\t\t\t\t\t\t\"maxLength\": 8,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"012345\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"required\": [\"challenge_token\", \"code\"],\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"challenge_token\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\",\n\t\t\t\t\t\"code\": \"012345\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"expires\": \"2025-02-04T20:40:46.340Z\",\n\t\t\t\t\t\t\t\t\"token\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/token-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/tokens/get.json",
    "content": "{\n\t\"operationId\": \"refreshToken\",\n\t\"summary\": \"Refresh your access token\",\n\t\"tags\": [\"tokens\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": []\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"expires\": \"2025-02-04T20:40:46.340Z\",\n\t\t\t\t\t\t\t\t\"token\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../components/token-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/tokens/post.json",
    "content": "{\n\t\"operationId\": \"requestToken\",\n\t\"summary\": \"Request a new access token from credentials\",\n\t\"tags\": [\"tokens\"],\n\t\"requestBody\": {\n\t\t\"description\": \"Credentials Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"identity\": {\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"me@example.com\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"scope\": {\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"enum\": [\"user\"],\n\t\t\t\t\t\t\t\"example\": \"user\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"secret\": {\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"bigredhorsebanana\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"required\": [\"identity\", \"secret\"],\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"identity\": \"me@example.com\",\n\t\t\t\t\t\"secret\": \"bigredhorsebanana\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"expires\": \"2025-02-04T20:40:46.340Z\",\n\t\t\t\t\t\t\t\t\"token\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"oneOf\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"$ref\": \"../../components/token-object.json\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"$ref\": \"../../components/token-challenge.json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/get.json",
    "content": "{\n\t\"operationId\": \"getUsers\",\n\t\"summary\": \"Get all users\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"expand\",\n\t\t\t\"description\": \"Expansions\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"enum\": [\"permissions\"]\n\t\t\t}\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:36:08.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Jamie Curnow\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"James\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"withPermissions\": {\n\t\t\t\t\t\t\t\"value\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:36:08.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"Jamie Curnow\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"James\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": [\"admin\"],\n\t\t\t\t\t\t\t\t\t\"permissions\": {\n\t\t\t\t\t\t\t\t\t\t\"visibility\": \"all\",\n\t\t\t\t\t\t\t\t\t\t\"proxy_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"redirection_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"dead_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"streams\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"access_lists\": \"manage\",\n\t\t\t\t\t\t\t\t\t\t\"certificates\": \"manage\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../components/user-list.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/post.json",
    "content": "{\n\t\"operationId\": \"createUser\",\n\t\"summary\": \"Create a User\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"User Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"required\": [\"name\", \"nickname\", \"email\"],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json#/properties/name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"nickname\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json#/properties/nickname\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"email\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json#/properties/email\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"roles\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json#/properties/roles\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"is_disabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json#/properties/is_disabled\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\t\"description\": \"Auth Credentials\",\n\t\t\t\t\t\t\t\"example\": {\n\t\t\t\t\t\t\t\t\"type\": \"password\",\n\t\t\t\t\t\t\t\t\"secret\": \"bigredhorsebanana\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"201\": {\n\t\t\t\"description\": \"201 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\"name\": \"Jamie Curnow\",\n\t\t\t\t\t\t\t\t\"nickname\": \"James\",\n\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\"roles\": [\"admin\"],\n\t\t\t\t\t\t\t\t\"permissions\": {\n\t\t\t\t\t\t\t\t\t\"id\": 3,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\t\"user_id\": 2,\n\t\t\t\t\t\t\t\t\t\"visibility\": \"user\",\n\t\t\t\t\t\t\t\t\t\"proxy_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\"redirection_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\"dead_hosts\": \"manage\",\n\t\t\t\t\t\t\t\t\t\"streams\": \"manage\",\n\t\t\t\t\t\t\t\t\t\"access_lists\": \"manage\",\n\t\t\t\t\t\t\t\t\t\"certificates\": \"manage\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../components/user-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/2fa/backup-codes/post.json",
    "content": "{\n\t\"operationId\": \"regenUser2faCodes\",\n\t\"summary\": \"Regenerate 2FA backup codes\",\n\t\"tags\": [\"users\"],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Verification Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"code\": {\n\t\t\t\t\t\t\t\"minLength\": 6,\n\t\t\t\t\t\t\t\"maxLength\": 8,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"123456\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"required\": [\"code\"],\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"code\": \"123456\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"backup_codes\": [\n\t\t\t\t\t\t\t\t\t\"6CD7CB06\",\n\t\t\t\t\t\t\t\t\t\"495302F3\",\n\t\t\t\t\t\t\t\t\t\"D8037852\",\n\t\t\t\t\t\t\t\t\t\"A6FFC956\",\n\t\t\t\t\t\t\t\t\t\"BC1A1851\",\n\t\t\t\t\t\t\t\t\t\"A05E644F\",\n\t\t\t\t\t\t\t\t\t\"A406D2E8\",\n\t\t\t\t\t\t\t\t\t\"0AE3C522\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"required\": [\"backup_codes\"],\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"backup_codes\": {\n\t\t\t\t\t\t\t\t\"description\": \"Backup codes\",\n\t\t\t\t\t\t\t\t\"example\": [\n\t\t\t\t\t\t\t\t\t\"6CD7CB06\",\n\t\t\t\t\t\t\t\t\t\"495302F3\",\n\t\t\t\t\t\t\t\t\t\"D8037852\",\n\t\t\t\t\t\t\t\t\t\"A6FFC956\",\n\t\t\t\t\t\t\t\t\t\"BC1A1851\",\n\t\t\t\t\t\t\t\t\t\"A05E644F\",\n\t\t\t\t\t\t\t\t\t\"A406D2E8\",\n\t\t\t\t\t\t\t\t\t\"0AE3C522\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\t\"example\": \"6CD7CB06\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/2fa/delete.json",
    "content": "{\n\t\"operationId\": \"disableUser2fa\",\n\t\"summary\": \"Disable 2fa for user\",\n\t\"tags\": [\"users\"],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t},\n\t\t{\n\t\t\t\"in\": \"query\",\n\t\t\t\"name\": \"code\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 6,\n\t\t\t\t\"maxLength\": 6,\n\t\t\t\t\"example\": \"012345\"\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"2fa Code\",\n\t\t\t\"example\": \"012345\"\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/2fa/enable/post.json",
    "content": "{\n\t\"operationId\": \"enableUser2fa\",\n\t\"summary\": \"Verify code and enable 2FA\",\n\t\"tags\": [\"users\"],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Verification Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"code\": {\n\t\t\t\t\t\t\t\"minLength\": 6,\n\t\t\t\t\t\t\t\"maxLength\": 8,\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"example\": \"123456\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"required\": [\"code\"],\n\t\t\t\t\t\"type\": \"object\"\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"code\": \"123456\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"backup_codes\": [\n\t\t\t\t\t\t\t\t\t\"6CD7CB06\",\n\t\t\t\t\t\t\t\t\t\"495302F3\",\n\t\t\t\t\t\t\t\t\t\"D8037852\",\n\t\t\t\t\t\t\t\t\t\"A6FFC956\",\n\t\t\t\t\t\t\t\t\t\"BC1A1851\",\n\t\t\t\t\t\t\t\t\t\"A05E644F\",\n\t\t\t\t\t\t\t\t\t\"A406D2E8\",\n\t\t\t\t\t\t\t\t\t\"0AE3C522\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"required\": [\"backup_codes\"],\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"backup_codes\": {\n\t\t\t\t\t\t\t\t\"description\": \"Backup codes\",\n\t\t\t\t\t\t\t\t\"example\": [\n\t\t\t\t\t\t\t\t\t\"6CD7CB06\",\n\t\t\t\t\t\t\t\t\t\"495302F3\",\n\t\t\t\t\t\t\t\t\t\"D8037852\",\n\t\t\t\t\t\t\t\t\t\"A6FFC956\",\n\t\t\t\t\t\t\t\t\t\"BC1A1851\",\n\t\t\t\t\t\t\t\t\t\"A05E644F\",\n\t\t\t\t\t\t\t\t\t\"A406D2E8\",\n\t\t\t\t\t\t\t\t\t\"0AE3C522\"\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\t\"example\": \"6CD7CB06\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/2fa/get.json",
    "content": "{\n\t\"operationId\": \"getUser2faStatus\",\n\t\"summary\": \"Get user 2fa Status\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": []\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"enabled\": false,\n\t\t\t\t\t\t\t\t\"backup_codes_remaining\": 0\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"required\": [\"enabled\", \"backup_codes_remaining\"],\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"enabled\": {\n\t\t\t\t\t\t\t\t\"type\": \"boolean\",\n\t\t\t\t\t\t\t\t\"description\": \"Is 2FA enabled for this user\",\n\t\t\t\t\t\t\t\t\"example\": true\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"backup_codes_remaining\": {\n\t\t\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\t\t\"description\": \"Number of remaining backup codes for this user\",\n\t\t\t\t\t\t\t\t\"example\": 5\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/2fa/post.json",
    "content": "{\n\t\"operationId\": \"setupUser2fa\",\n\t\"summary\": \"Start 2FA setup, returns QR code URL\",\n\t\"tags\": [\"users\"],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"secret\": \"JZYCEBIEEJYUGPQM\",\n\t\t\t\t\t\t\t\t\"otpauth_url\": \"otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"required\": [\"secret\", \"otpauth_url\"],\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"secret\": {\n\t\t\t\t\t\t\t\t\"description\": \"TOTP Secret\",\n\t\t\t\t\t\t\t\t\"example\": \"JZYCEBIEEJYUGPQM\",\n\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"otpauth_url\": {\n\t\t\t\t\t\t\t\t\"description\": \"OTP Auth URL for QR Code generation\",\n\t\t\t\t\t\t\t\t\"example\": \"otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager\",\n\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"description\": \"200 response\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/auth/put.json",
    "content": "{\n\t\"operationId\": \"updateUserAuth\",\n\t\"summary\": \"Update a User's Authentication\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"oneOf\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^me$\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\"minimum\": 1\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID or 'me' for yourself\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Auth Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"required\": [\"type\", \"secret\"],\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"type\": {\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"pattern\": \"^password$\",\n\t\t\t\t\t\t\t\"example\": \"password\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"current\": {\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"minLength\": 1,\n\t\t\t\t\t\t\t\"maxLength\": 64,\n\t\t\t\t\t\t\t\"example\": \"changeme\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"secret\": {\n\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\"minLength\": 8,\n\t\t\t\t\t\t\t\"maxLength\": 64,\n\t\t\t\t\t\t\t\"example\": \"mySuperN3wP@ssword!\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/delete.json",
    "content": "{\n\t\"operationId\": \"deleteUser\",\n\t\"summary\": \"Delete a User\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/get.json",
    "content": "{\n\t\"operationId\": \"getUser\",\n\t\"summary\": \"Get a user\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"oneOf\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^me$\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\"minimum\": 1\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID or 'me' for yourself\",\n\t\t\t\"example\": 1\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:36:08.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\"name\": \"Jamie Curnow\",\n\t\t\t\t\t\t\t\t\"nickname\": \"James\",\n\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/login/post.json",
    "content": "{\n\t\"operationId\": \"loginAsUser\",\n\t\"summary\": \"Login as this user\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"token\": \"eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg\",\n\t\t\t\t\t\t\t\t\"expires\": \"2020-01-31T10:56:23.239Z\",\n\t\t\t\t\t\t\t\t\"user\": {\n\t\t\t\t\t\t\t\t\t\"id\": 1,\n\t\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T10:43:44.000Z\",\n\t\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T10:43:44.000Z\",\n\t\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\t\"email\": \"user2@example.com\",\n\t\t\t\t\t\t\t\t\t\"name\": \"John Doe\",\n\t\t\t\t\t\t\t\t\t\"nickname\": \"Jonny\",\n\t\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm\",\n\t\t\t\t\t\t\t\t\t\"roles\": []\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"description\": \"Login object\",\n\t\t\t\t\t\t\"required\": [\"expires\", \"token\", \"user\"],\n\t\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\t\"token\": {\n\t\t\t\t\t\t\t\t\"description\": \"JWT Token\",\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"example\": \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"expires\": {\n\t\t\t\t\t\t\t\t\"description\": \"Token Expiry Timestamp\",\n\t\t\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\t\t\"example\": \"2020-01-30T10:43:44.000Z\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"user\": {\n\t\t\t\t\t\t\t\t\"$ref\": \"../../../../components/user-object.json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/permissions/put.json",
    "content": "{\n\t\"operationId\": \"updateUserPermissions\",\n\t\"summary\": \"Update a User's Permissions\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\"minimum\": 1\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"Permissions Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"$ref\": \"../../../../components/permission-object.json\"\n\t\t\t\t},\n\t\t\t\t\"example\": {\n\t\t\t\t\t\"visibility\": \"all\",\n\t\t\t\t\t\"access_lists\": \"view\",\n\t\t\t\t\t\"certificates\": \"hidden\",\n\t\t\t\t\t\"dead_hosts\": \"hidden\",\n\t\t\t\t\t\"proxy_hosts\": \"manage\",\n\t\t\t\t\t\"redirection_hosts\": \"hidden\",\n\t\t\t\t\t\"streams\": \"hidden\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": true\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"type\": \"boolean\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/users/userID/put.json",
    "content": "{\n\t\"operationId\": \"updateUser\",\n\t\"summary\": \"Update a User\",\n\t\"tags\": [\"users\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth\": [\"admin\"]\n\t\t}\n\t],\n\t\"parameters\": [\n\t\t{\n\t\t\t\"in\": \"path\",\n\t\t\t\"name\": \"userID\",\n\t\t\t\"schema\": {\n\t\t\t\t\"oneOf\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"pattern\": \"^me$\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\"minimum\": 1\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"required\": true,\n\t\t\t\"description\": \"User ID or 'me' for yourself\",\n\t\t\t\"example\": 2\n\t\t}\n\t],\n\t\"requestBody\": {\n\t\t\"description\": \"User Payload\",\n\t\t\"required\": true,\n\t\t\"content\": {\n\t\t\t\"application/json\": {\n\t\t\t\t\"schema\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"additionalProperties\": false,\n\t\t\t\t\t\"minProperties\": 1,\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"name\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json#/properties/name\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"nickname\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json#/properties/nickname\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"email\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json#/properties/email\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"roles\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json#/properties/roles\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"is_disabled\": {\n\t\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json#/properties/is_disabled\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"id\": 2,\n\t\t\t\t\t\t\t\t\"created_on\": \"2020-01-30T09:36:08.000Z\",\n\t\t\t\t\t\t\t\t\"modified_on\": \"2020-01-30T09:41:04.000Z\",\n\t\t\t\t\t\t\t\t\"is_disabled\": false,\n\t\t\t\t\t\t\t\t\"email\": \"jc@jc21.com\",\n\t\t\t\t\t\t\t\t\"name\": \"Jamie Curnow\",\n\t\t\t\t\t\t\t\t\"nickname\": \"James\",\n\t\t\t\t\t\t\t\t\"avatar\": \"//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm\",\n\t\t\t\t\t\t\t\t\"roles\": [\"admin\"]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/user-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/paths/version/check/get.json",
    "content": "{\n\t\"operationId\": \"checkVersion\",\n\t\"summary\": \"Returns any new version data from github\",\n\t\"tags\": [\"public\"],\n\t\"responses\": {\n\t\t\"200\": {\n\t\t\t\"description\": \"200 response\",\n\t\t\t\"content\": {\n\t\t\t\t\"application/json\": {\n\t\t\t\t\t\"examples\": {\n\t\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"current\": \"v2.12.0\",\n\t\t\t\t\t\t\t\t\"latest\": \"v2.13.4\",\n\t\t\t\t\t\t\t\t\"update_available\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"schema\": {\n\t\t\t\t\t\t\"$ref\": \"../../../components/check-version-object.json\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/schema/swagger.json",
    "content": "{\n\t\"openapi\": \"3.1.0\",\n\t\"info\": {\n\t\t\"title\": \"Nginx Proxy Manager API\",\n\t\t\"version\": \"2.x.x\",\n\t\t\"description\": \"This is the official API documentation for Nginx Proxy Manager.\\n\\nMost endpoints require authentication via Bearer Token (JWT). You can generate a token by logging in via the `POST /tokens` endpoint.\\n\\nFor more information, visit the [Nginx Proxy Manager Documentation](https://nginxproxymanager.com).\"\n\t},\n\t\"servers\": [\n\t\t{\n\t\t\t\"url\": \"http://127.0.0.1:81/api\"\n\t\t}\n\t],\n\t\"components\": {\n\t\t\"securitySchemes\": {\n\t\t\t\"$ref\": \"./components/security-schemes.json\"\n\t\t}\n\t},\n\t\"tags\": [\n\t\t{\n\t\t\t\"name\": \"public\",\n\t\t\t\"description\": \"Endpoints that do not require authentication\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"audit-log\",\n\t\t\t\"description\": \"Endpoints related to Audit Logs\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"access-lists\",\n\t\t\t\"description\": \"Endpoints related to Access Lists\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"certificates\",\n\t\t\t\"description\": \"Endpoints related to Certificates\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"404-hosts\",\n\t\t\t\"description\": \"Endpoints related to 404 Hosts\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"proxy-hosts\",\n\t\t\t\"description\": \"Endpoints related to Proxy Hosts\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"redirection-hosts\",\n\t\t\t\"description\": \"Endpoints related to Redirection Hosts\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"streams\",\n\t\t\t\"description\": \"Endpoints related to Streams\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"reports\",\n\t\t\t\"description\": \"Endpoints for viewing reports\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"settings\",\n\t\t\t\"description\": \"Endpoints for managing application settings\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"tokens\",\n\t\t\t\"description\": \"Endpoints for managing authentication tokens\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"users\",\n\t\t\t\"description\": \"Endpoints for managing users\"\n\t\t}\n\t],\n\t\"paths\": {\n\t\t\"/\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/audit-log\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/audit-log/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/audit-log/{id}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/audit-log/id/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/access-lists\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/access-lists/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/access-lists/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/access-lists/{listID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/access-lists/listID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/access-lists/listID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/access-lists/listID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/dns-providers\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/dns-providers/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/validate\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/validate/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/test-http\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/test-http/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/{certID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/certID/get.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/certID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/{certID}/download\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/certID/download/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/{certID}/renew\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/certID/renew/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/certificates/{certID}/upload\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/certificates/certID/upload/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/proxy-hosts\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/proxy-hosts/{hostID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/hostID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/hostID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/hostID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/proxy-hosts/{hostID}/enable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/hostID/enable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/proxy-hosts/{hostID}/disable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/proxy-hosts/hostID/disable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/redirection-hosts\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/redirection-hosts/{hostID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/hostID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/hostID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/hostID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/redirection-hosts/{hostID}/enable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/hostID/enable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/redirection-hosts/{hostID}/disable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/redirection-hosts/hostID/disable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/dead-hosts\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/dead-hosts/{hostID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/hostID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/hostID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/hostID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/dead-hosts/{hostID}/enable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/hostID/enable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/dead-hosts/{hostID}/disable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/dead-hosts/hostID/disable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/streams\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/streams/{streamID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/streamID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/streamID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/streamID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/streams/{streamID}/enable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/streamID/enable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/nginx/streams/{streamID}/disable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/nginx/streams/streamID/disable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/reports/hosts\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/reports/hosts/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/schema\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/schema/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/settings\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/settings/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/settings/{settingID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/settings/settingID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/settings/settingID/put.json\"\n\t\t\t}\n\t\t},\n\t\t\"/tokens\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/tokens/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/tokens/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/tokens/2fa\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/tokens/2fa/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/version/check\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/version/check/get.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/users/get.json\"\n\t\t\t},\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/users/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/get.json\"\n\t\t\t},\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/put.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/2fa\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/2fa/post.json\"\n\t\t\t},\n\t\t\t\"get\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/2fa/get.json\"\n\t\t\t},\n\t\t\t\"delete\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/2fa/delete.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/2fa/enable\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/2fa/enable/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/2fa/backup-codes\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/2fa/backup-codes/post.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/auth\": {\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/auth/put.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/permissions\": {\n\t\t\t\"put\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/permissions/put.json\"\n\t\t\t}\n\t\t},\n\t\t\"/users/{userID}/login\": {\n\t\t\t\"post\": {\n\t\t\t\t\"$ref\": \"./paths/users/userID/login/post.json\"\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "backend/scripts/install-certbot-plugins",
    "content": "#!/usr/bin/node\n\n// Usage:\n//   Install all plugins defined in `../certbot/dns-plugins.json`:\n//    ./install-certbot-plugins\n//   Install one or more specific plugins:\n//    ./install-certbot-plugins route53 cloudflare\n//\n// Usage with a running docker container:\n//    docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c \"/app/scripts/install-certbot-plugins\"\n//\n\nimport batchflow from \"batchflow\";\nimport dnsPlugins from \"../certbot/dns-plugins.json\" with { type: \"json\" };\nimport { installPlugin } from \"../lib/certbot.js\";\nimport { certbot as logger } from \"../logger.js\";\n\nlet hasErrors = false;\nconst failingPlugins = [];\n\nlet pluginKeys = Object.keys(dnsPlugins);\nif (process.argv.length > 2) {\n\tpluginKeys = process.argv.slice(2);\n}\n\nbatchflow(pluginKeys)\n\t.sequential()\n\t.each((i, pluginKey, next) => {\n\t\tinstallPlugin(pluginKey)\n\t\t\t.then(() => {\n\t\t\t\tnext();\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\thasErrors = true;\n\t\t\t\tfailingPlugins.push(pluginKey);\n\t\t\t\tnext(err);\n\t\t\t});\n\t})\n\t.error((err) => {\n\t\tlogger.error(err.message);\n\t})\n\t.end(() => {\n\t\tif (hasErrors) {\n\t\t\tlogger.error(\n\t\t\t\t\"Some plugins failed to install. Please check the logs above. Failing plugins: \" +\n\t\t\t\t\t\"\\n - \" +\n\t\t\t\t\tfailingPlugins.join(\"\\n - \"),\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t} else {\n\t\t\tlogger.complete(\"Plugins installed successfully\");\n\t\t\tprocess.exit(0);\n\t\t}\n\t});\n"
  },
  {
    "path": "backend/scripts/regenerate-config",
    "content": "#!/usr/bin/env node\n\nimport * as process from \"node:process\"; // Use the node: protocol for built-ins\nimport internalNginx from \"../internal/nginx.js\";\nimport { global as logger } from \"../logger.js\";\nimport deadHostModel from \"../models/dead_host.js\";\nimport proxyHostModel from \"../models/proxy_host.js\";\nimport redirectionHostModel from \"../models/redirection_host.js\";\nimport streamModel from \"../models/stream.js\";\n\nconst args = process.argv.slice(2);\nconst UNATTENDED = args.includes(\"-y\") || args.includes(\"--yes\");\nconst DRY_RUN = args.includes(\"--dry-run\");\n\nif (args.includes(\"--help\") || args.includes(\"-h\")) {\n\tconsole.log(\"\\nThis will iterate over all Hosts and regnerate their Nginx configs.\\n\")\n\tconsole.log(\"Usage: ./regenerate-config [-h|--help] [-y|--yes] [--dry-run]\\n\");\n\tprocess.exit(0);\n}\n\n// ask for the user to confirm the action if not in unattended mode\nif (!UNATTENDED && !DRY_RUN) {\n\tconst readline = await import(\"node:readline\");\n\tconst rl = readline.createInterface({\n\t\tinput: process.stdin,\n\t\toutput: process.stdout,\n\t});\n\n\tconst question = (query) =>\n\t\tnew Promise((resolve) => rl.question(query, resolve));\n\n\tconst answer = await question(\n\t\t\"This will iterate over all Hosts and regnerate their Nginx configs.\\n\\nAre you sure you want to proceed? (y/N) \",\n\t);\n\trl.close();\n\n\tif (answer.toLowerCase() !== \"y\") {\n\t\tconsole.log(\"Aborting.\");\n\t\tprocess.exit(0);\n\t}\n}\n\nconst logIt = (msg, type = \"info\") => logger[type](\n\t`${DRY_RUN ? '[DRY RUN] ' : ''}${msg}`,\n);\n\n// Let's do it.\n\nconst processItems = async (model, type) => {\n\tconst rows = await model\n\t\t.query()\n\t\t.where(\"is_deleted\", 0)\n\t\t.andWhere(\"enabled\", 1)\n\t\t.groupBy(\"id\")\n\t\t.allowGraph(model.defaultAllowGraph)\n\t\t.withGraphFetched(`[${model.defaultExpand.join(\", \")}]`)\n\t\t.orderBy(...model.defaultOrder);\n\n\tlogIt(`[${type}] Found ${rows.length} rows to process...`);\n\tfor (const row of rows) {\n\t\tif (!DRY_RUN) {\n\t\t\tlogIt(`[${type}] Regenerating config #${row.id}: ${row.domain_names ? row.domain_names.join(\", \") : 'port ' + row.incoming_port}`);\n\t\t\tawait internalNginx.configure(proxyHostModel, \"proxy_host\", row);\n\t\t} else {\n\t\t\tlogIt(`[${type}] Skipping generation of config #${row.id}: ${row.domain_names ? row.domain_names.join(\", \") : 'port ' + row.incoming_port}`);\n\t\t}\n\t}\n};\n\nawait processItems(proxyHostModel, \"Proxy Host\");\nawait processItems(redirectionHostModel, \"Redirection Host\");\nawait processItems(deadHostModel, \"404 Host\");\nawait processItems(streamModel, \"Stream\");\n\nlogIt(\"Completed\", \"success\");\nprocess.exit(0);\n"
  },
  {
    "path": "backend/setup.js",
    "content": "import { installPlugins } from \"./lib/certbot.js\";\nimport utils from \"./lib/utils.js\";\nimport { setup as logger } from \"./logger.js\";\nimport authModel from \"./models/auth.js\";\nimport certificateModel from \"./models/certificate.js\";\nimport settingModel from \"./models/setting.js\";\nimport userModel from \"./models/user.js\";\nimport userPermissionModel from \"./models/user_permission.js\";\n\nexport const isSetup = async () => {\n\tconst row = await userModel.query().select(\"id\").where(\"is_deleted\", 0).first();\n\treturn row?.id > 0;\n}\n\n/**\n * Creates a default admin users if one doesn't already exist in the database\n *\n * @returns {Promise}\n */\nconst setupDefaultUser = async () => {\n\tconst initialAdminEmail = process.env.INITIAL_ADMIN_EMAIL;\n\tconst initialAdminPassword = process.env.INITIAL_ADMIN_PASSWORD;\n\n\t// This will only create a new user when there are no active users in the database\n\t// and the INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD environment variables are set.\n\t// Otherwise, users should be shown the setup wizard in the frontend.\n\t// I'm keeping this legacy behavior in case some people are automating deployments.\n\n\tif (!initialAdminEmail || !initialAdminPassword) {\n\t\treturn Promise.resolve();\n\t}\n\n\tconst userIsetup = await isSetup();\n\tif (!userIsetup) {\n\t\t// Create a new user and set password\n\t\tlogger.info(`Creating a new user: ${initialAdminEmail} with password: ${initialAdminPassword}`);\n\n\t\tconst data = {\n\t\t\tis_deleted: 0,\n\t\t\temail: initialAdminEmail,\n\t\t\tname: \"Administrator\",\n\t\t\tnickname: \"Admin\",\n\t\t\tavatar: \"\",\n\t\t\troles: [\"admin\"],\n\t\t};\n\n\t\tconst user = await userModel\n\t\t\t.query()\n\t\t\t.insertAndFetch(data);\n\n\t\tawait authModel\n\t\t\t.query()\n\t\t\t.insert({\n\t\t\t\tuser_id: user.id,\n\t\t\t\ttype: \"password\",\n\t\t\t\tsecret: initialAdminPassword,\n\t\t\t\tmeta: {},\n\t\t\t});\n\n\t\tawait userPermissionModel.query().insert({\n\t\t\tuser_id: user.id,\n\t\t\tvisibility: \"all\",\n\t\t\tproxy_hosts: \"manage\",\n\t\t\tredirection_hosts: \"manage\",\n\t\t\tdead_hosts: \"manage\",\n\t\t\tstreams: \"manage\",\n\t\t\taccess_lists: \"manage\",\n\t\t\tcertificates: \"manage\",\n\t\t});\n\t\tlogger.info(\"Initial admin setup completed\");\n\t}\n};\n\n/**\n * Creates default settings if they don't already exist in the database\n *\n * @returns {Promise}\n */\nconst setupDefaultSettings = async () => {\n\tconst row = await settingModel\n\t\t.query()\n\t\t.select(\"id\")\n\t\t.where({ id: \"default-site\" })\n\t\t.first();\n\n\tif (!row?.id) {\n\t\tawait settingModel\n\t\t\t.query()\n\t\t\t.insert({\n\t\t\t\tid: \"default-site\",\n\t\t\t\tname: \"Default Site\",\n\t\t\t\tdescription: \"What to show when Nginx is hit with an unknown Host\",\n\t\t\t\tvalue: \"congratulations\",\n\t\t\t\tmeta: {},\n\t\t\t});\n\t\tlogger.info(\"Default settings added\");\n\t}\n};\n\n/**\n * Installs all Certbot plugins which are required for an installed certificate\n *\n * @returns {Promise}\n */\nconst setupCertbotPlugins = async () => {\n\tconst certificates = await certificateModel\n\t\t.query()\n\t\t.where(\"is_deleted\", 0)\n\t\t.andWhere(\"provider\", \"letsencrypt\");\n\n\tif (certificates?.length) {\n\t\tconst plugins = [];\n\t\tconst promises = [];\n\n\t\tcertificates.map((certificate) => {\n\t\t\tif (certificate.meta && certificate.meta.dns_challenge === true) {\n\t\t\t\tif (plugins.indexOf(certificate.meta.dns_provider) === -1) {\n\t\t\t\t\tplugins.push(certificate.meta.dns_provider);\n\t\t\t\t}\n\n\t\t\t\t// Make sure credentials file exists\n\t\t\t\tconst credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;\n\t\t\t\t// Escape single quotes and backslashes\n\t\t\t\tif (typeof certificate.meta.dns_provider_credentials === \"string\") {\n\t\t\t\t\tconst escapedCredentials = certificate.meta.dns_provider_credentials\n\t\t\t\t\t\t.replaceAll(\"'\", \"\\\\'\")\n\t\t\t\t\t\t.replaceAll(\"\\\\\", \"\\\\\\\\\");\n\t\t\t\t\tconst credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;\n\t\t\t\t\tpromises.push(utils.exec(credentials_cmd));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\n\t\tawait installPlugins(plugins);\n\n\t\tif (promises.length) {\n\t\t\tawait Promise.all(promises);\n\t\t\tlogger.info(`Added Certbot plugins ${plugins.join(\", \")}`);\n\t\t}\n\t}\n};\n\n/**\n * Starts a timer to call run the logrotation binary every two days\n * @returns {Promise}\n */\nconst setupLogrotation = () => {\n\tconst intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days\n\n\tconst runLogrotate = async () => {\n\t\ttry {\n\t\t\tawait utils.exec(\"logrotate /etc/logrotate.d/nginx-proxy-manager\");\n\t\t\tlogger.info(\"Logrotate completed.\");\n\t\t} catch (e) {\n\t\t\tlogger.warn(e);\n\t\t}\n\t};\n\n\tlogger.info(\"Logrotate Timer initialized\");\n\tsetInterval(runLogrotate, intervalTimeout);\n\t// And do this now as well\n\treturn runLogrotate();\n};\n\nexport default () => setupDefaultUser().then(setupDefaultSettings).then(setupCertbotPlugins).then(setupLogrotation);\n"
  },
  {
    "path": "backend/templates/_access.conf",
    "content": "{% if access_list_id > 0 %}\n    {% if access_list.items.length > 0 %}\n    # Authorization\n    auth_basic            \"Authorization required\";\n    auth_basic_user_file  /data/access/{{ access_list_id }};\n\n    {% if access_list.pass_auth == 0 or access_list.pass_auth == false %}\n    proxy_set_header Authorization \"\";\n    {% endif %}\n\n    {% endif %}\n\n    # Access Rules: {{ access_list.clients | size }} total\n    {% for client in access_list.clients %}\n    {{client | nginxAccessRule}}\n    {% endfor %}\n    deny all;\n\n    # Access checks must...\n    {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %}\n    satisfy any;\n    {% else %}\n    satisfy all;\n    {% endif %}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/_assets.conf",
    "content": "{% if caching_enabled == 1 or caching_enabled == true -%}\n  # Asset Caching\n  include conf.d/include/assets.conf;\n{% endif %}"
  },
  {
    "path": "backend/templates/_certificates.conf",
    "content": "{% if certificate and certificate_id > 0 -%}\n{% if certificate.provider == \"letsencrypt\" %}\n  # Let's Encrypt SSL\n  include conf.d/include/letsencrypt-acme-challenge.conf;\n  include conf.d/include/ssl-cache.conf;\n  include conf.d/include/ssl-ciphers.conf;\n  ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;\n{% else %}\n  # Custom SSL\n  ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;\n  ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;\n{% endif %}\n{% endif %}\n\n"
  },
  {
    "path": "backend/templates/_certificates_stream.conf",
    "content": "{% if certificate and certificate_id > 0 %}\n{% if certificate.provider == \"letsencrypt\" %}\n  # Let's Encrypt SSL\n  include conf.d/include/ssl-cache-stream.conf;\n  include conf.d/include/ssl-ciphers.conf;\n  ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;\n{%- else %}\n  # Custom SSL\n  ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;\n  ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;\n{%- endif -%}\n{%- endif -%}\n"
  },
  {
    "path": "backend/templates/_exploits.conf",
    "content": "{% if block_exploits == 1 or block_exploits == true %}\n  # Block Exploits\n  include conf.d/include/block-exploits.conf;\n{% endif %}"
  },
  {
    "path": "backend/templates/_forced_ssl.conf",
    "content": "{% if certificate and certificate_id > 0 -%}\n{% if ssl_forced == 1 or ssl_forced == true %}\n    # Force SSL\n    {% if trust_forwarded_proto == true %}\n    set $trust_forwarded_proto \"T\";\n    {% else %}\n    set $trust_forwarded_proto \"F\";\n    {% endif %}\n    include conf.d/include/force-ssl.conf;\n{% endif %}\n{% endif %}"
  },
  {
    "path": "backend/templates/_header_comment.conf",
    "content": "# ------------------------------------------------------------\n# {{ domain_names | join: \", \" }}\n# ------------------------------------------------------------"
  },
  {
    "path": "backend/templates/_hsts.conf",
    "content": "{% if certificate and certificate_id > 0 -%}\n{% if ssl_forced == 1 or ssl_forced == true %}\n{% if hsts_enabled == 1 or hsts_enabled == true %}\n  # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)\n  add_header Strict-Transport-Security $hsts_header always;\n{% endif %}\n{% endif %}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/_hsts_map.conf",
    "content": "map $scheme $hsts_header {\n    https   \"max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload\";\n}"
  },
  {
    "path": "backend/templates/_listen.conf",
    "content": "  listen 80;\n{% if ipv6 -%}\n  listen [::]:80;\n{% else -%}\n  #listen [::]:80;\n{% endif %}\n{% if certificate -%}\n  listen 443 ssl;\n{% if ipv6 -%}\n  listen [::]:443 ssl;\n{% else -%}\n  #listen [::]:443;\n{% endif %}\n{% endif %}\n  server_name {{ domain_names | join: \" \" }};\n{% if http2_support == 1 or http2_support == true %}\n  http2 on;\n{% else -%}\n  http2 off;\n{% endif %}"
  },
  {
    "path": "backend/templates/_location.conf",
    "content": "  location {{ path }} {\n    {{ advanced_config }}\n\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Scheme $scheme;\n    proxy_set_header X-Forwarded-Proto  $scheme;\n    proxy_set_header X-Forwarded-For    $remote_addr;\n    proxy_set_header X-Real-IP\t\t$remote_addr;\n\n    proxy_pass       {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};\n\n    {% include \"_access.conf\" %}\n    {% include \"_assets.conf\" %}\n    {% include \"_exploits.conf\" %}\n    {% include \"_forced_ssl.conf\" %}\n    {% include \"_hsts.conf\" %}\n\n    {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection $http_connection;\n    proxy_http_version 1.1;\n    {% endif %}\n  }\n\n"
  },
  {
    "path": "backend/templates/dead_host.conf",
    "content": "{% include \"_header_comment.conf\" %}\n\n{% if enabled %}\n\n{% include \"_hsts_map.conf\" %}\n\nserver {\n{% include \"_listen.conf\" %}\n{% include \"_certificates.conf\" %}\n{% include \"_hsts.conf\" %}\n{% include \"_forced_ssl.conf\" %}\n\n  access_log /data/logs/dead-host-{{ id }}_access.log standard;\n  error_log /data/logs/dead-host-{{ id }}_error.log warn;\n\n{{ advanced_config }}\n\n{% if use_default_location %}\n  location / {\n{% include \"_hsts.conf\" %}\n    return 404;\n  }\n{% endif %}\n\n  # Custom\n  include /data/nginx/custom/server_dead[.]conf;\n}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/default.conf",
    "content": "# ------------------------------------------------------------\n# Default Site\n# ------------------------------------------------------------\n{% if value == \"congratulations\" %}\n# Skipping output, congratulations page configration is baked in.\n{%- else %}\nserver {\n  listen 80 default;\n{% if ipv6 -%}\n  listen [::]:80 default;\n{% else -%}\n  #listen [::]:80 default;\n{% endif %}\n  server_name default-host.localhost;\n  access_log /data/logs/default-host_access.log combined;\n  error_log /data/logs/default-host_error.log warn;\n{% include \"_exploits.conf\" %}\n\n  include conf.d/include/letsencrypt-acme-challenge.conf;\n\n{%- if value == \"404\" %}\n  location / {\n    return 404;\n  }\n{% endif %}\n\n{%- if value == \"444\" %}\n  location / {\n    return 444;\n  }\n{% endif %}\n\n{%- if value == \"redirect\" %}\n  location / {\n    return 301 {{ meta.redirect }};\n  }\n{%- endif %}\n\n{%- if value == \"html\" %}\n  root /data/nginx/default_www;\n  location / {\n    try_files $uri /index.html;\n  }\n{%- endif %}\n}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/ip_ranges.conf",
    "content": "{% for range in ip_ranges %}\nset_real_ip_from {{ range }};\n{% endfor %}"
  },
  {
    "path": "backend/templates/letsencrypt-request.conf",
    "content": "{% include \"_header_comment.conf\" %}\n\nserver {\n  listen 80;\n{% if ipv6 -%}\n  listen [::]:80;\n{% endif %}\n\n  server_name {{ domain_names | join: \" \" }};\n\n  access_log /data/logs/letsencrypt-requests_access.log standard;\n  error_log /data/logs/letsencrypt-requests_error.log warn;\n\n  include conf.d/include/letsencrypt-acme-challenge.conf;\n\n  location / {\n    return 404;\n  }\n}\n"
  },
  {
    "path": "backend/templates/proxy_host.conf",
    "content": "{% include \"_header_comment.conf\" %}\n\n{% if enabled %}\n\n{% include \"_hsts_map.conf\" %}\n\nserver {\n  set $forward_scheme {{ forward_scheme }};\n  set $server         \"{{ forward_host }}\";\n  set $port           {{ forward_port }};\n\n{% include \"_listen.conf\" %}\n{% include \"_certificates.conf\" %}\n{% include \"_assets.conf\" %}\n{% include \"_exploits.conf\" %}\n{% include \"_hsts.conf\" %}\n{% include \"_forced_ssl.conf\" %}\n\n{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}\nproxy_set_header Upgrade $http_upgrade;\nproxy_set_header Connection $http_connection;\nproxy_http_version 1.1;\n{% endif %}\n\n  access_log /data/logs/proxy-host-{{ id }}_access.log proxy;\n  error_log /data/logs/proxy-host-{{ id }}_error.log warn;\n\n{{ advanced_config }}\n\n{{ locations }}\n\n{% if use_default_location %}\n\n  location / {\n\n{% include \"_access.conf\" %}\n{% include \"_hsts.conf\" %}\n\n    {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection $http_connection;\n    proxy_http_version 1.1;\n    {% endif %}\n\n    # Proxy!\n    include conf.d/include/proxy.conf;\n  }\n{% endif %}\n\n  # Custom\n  include /data/nginx/custom/server_proxy[.]conf;\n}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/redirection_host.conf",
    "content": "{% include \"_header_comment.conf\" %}\n\n{% if enabled %}\n\n{% include \"_hsts_map.conf\" %}\n\nserver {\n{% include \"_listen.conf\" %}\n{% include \"_certificates.conf\" %}\n{% include \"_assets.conf\" %}\n{% include \"_exploits.conf\" %}\n{% include \"_hsts.conf\" %}\n{% include \"_forced_ssl.conf\" %}\n\n  access_log /data/logs/redirection-host-{{ id }}_access.log standard;\n  error_log /data/logs/redirection-host-{{ id }}_error.log warn;\n\n{{ advanced_config }}\n\n{% if use_default_location %}\n  location / {\n{% include \"_hsts.conf\" %}\n\n    {% if preserve_path == 1 or preserve_path == true %}\n        return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri;\n    {% else %}\n        return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }};\n    {% endif %}\n  }\n{% endif %}\n\n  # Custom\n  include /data/nginx/custom/server_redirect[.]conf;\n}\n{% endif %}\n"
  },
  {
    "path": "backend/templates/stream.conf",
    "content": "# ------------------------------------------------------------\n# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }}\n# ------------------------------------------------------------\n\n{% if enabled %}\n{% if tcp_forwarding == 1 or tcp_forwarding == true -%}\nserver {\n  listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};\n  {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};\n\n  {%- include \"_certificates_stream.conf\" %}\n\n  proxy_pass {{ forwarding_host }}:{{ forwarding_port }};\n\n  access_log /data/logs/stream-{{ id }}_access.log stream;\n  error_log /data/logs/stream-{{ id }}_error.log warn;\n\n  # Custom\n  include /data/nginx/custom/server_stream[.]conf;\n  include /data/nginx/custom/server_stream_tcp[.]conf;\n}\n{% endif %}\n\n{% if udp_forwarding == 1 or udp_forwarding == true -%}\nserver {\n  listen {{ incoming_port }} udp;\n  {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;\n\n  proxy_pass {{ forwarding_host }}:{{ forwarding_port }};\n\n  access_log /data/logs/stream-{{ id }}_access.log stream;\n  error_log /data/logs/stream-{{ id }}_error.log warn;\n\n  # Custom\n  include /data/nginx/custom/server_stream[.]conf;\n  include /data/nginx/custom/server_stream_udp[.]conf;\n}\n{% endif %}\n{% endif %}\n"
  },
  {
    "path": "backend/validate-schema.js",
    "content": "#!/usr/bin/node\n\nimport SwaggerParser from \"@apidevtools/swagger-parser\";\nimport chalk from \"chalk\";\nimport { getCompiledSchema } from \"./schema/index.js\";\n\nconst log = console.log;\n\ngetCompiledSchema().then(async (swaggerJSON) => {\n\ttry {\n\t\tconst api = await SwaggerParser.validate(swaggerJSON);\n\t\tconsole.log(\"API name: %s, Version: %s\", api.info.title, api.info.version);\n\t\tlog(chalk.green(\"❯ Schema is valid\"));\n\t} catch (e) {\n\t\tconsole.error(e);\n\t\tlog(chalk.red(\"❯\", e.message), \"\\n\");\n\t\tprocess.exit(1);\n\t}\n});\n"
  },
  {
    "path": "docker/.dive-ci",
    "content": "rules:\n  # If the efficiency is measured below X%, mark as failed.\n  # Expressed as a ratio between 0-1.\n  lowestEfficiency: 0.99\n\n  # If the amount of wasted space is at least X or larger than X, mark as failed.\n  # Expressed in B, KB, MB, and GB.\n  highestWastedBytes: 15MB\n\n  # If the amount of wasted space makes up for X% or more of the image, mark as failed.\n  # Note: the base image layer is NOT included in the total image size.\n  # Expressed as a ratio between 0-1; fails if the threshold is met or crossed.\n  highestUserWastedPercent: 0.02\n\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "# This is a Dockerfile intended to be built using `docker buildx`\n# for multi-arch support. Building with `docker build` may have unexpected results.\n\n# This file assumes that the frontend has been built using ./scripts/frontend-build\n\nFROM nginxproxymanager/testca AS testca\nFROM nginxproxymanager/nginx-full:certbot-node\n\nARG TARGETPLATFORM\nARG BUILD_VERSION\nARG BUILD_COMMIT\nARG BUILD_DATE\n\n# See: https://github.com/just-containers/s6-overlay/blob/master/README.md\nENV SUPPRESS_NO_CONFIG_WARNING=1 \\\n\tS6_BEHAVIOUR_IF_STAGE2_FAILS=1 \\\n\tS6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \\\n\tS6_FIX_ATTRS_HIDDEN=1 \\\n\tS6_KILL_FINISH_MAXTIME=10000 \\\n\tS6_VERBOSITY=1 \\\n\tNODE_ENV=production \\\n\tNPM_BUILD_VERSION=\"${BUILD_VERSION}\" \\\n\tNPM_BUILD_COMMIT=\"${BUILD_COMMIT}\" \\\n\tNPM_BUILD_DATE=\"${BUILD_DATE}\" \\\n\tNODE_OPTIONS=\"--openssl-legacy-provider\"\n\nRUN echo \"fs.file-max = 65535\" > /etc/sysctl.conf \\\n\t&& apt-get update \\\n\t&& apt-get install -y --no-install-recommends jq logrotate \\\n\t&& apt-get clean \\\n\t&& rm -rf /var/lib/apt/lists/*\n\n# s6 overlay\nCOPY docker/scripts/install-s6 /tmp/install-s6\nRUN /tmp/install-s6 \"${TARGETPLATFORM}\" && rm -f /tmp/install-s6\n\nEXPOSE 80 81 443\n\nCOPY backend       /app\nCOPY frontend/dist /app/frontend\n\nWORKDIR /app\nRUN yarn install \\\n\t&& yarn cache clean\n\n# add late to limit cache-busting by modifications\nCOPY docker/rootfs /\nCOPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt\n\n# Remove frontend service not required for prod, dev nginx config as well\nRUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \\\n\t&& chmod 644 /etc/logrotate.d/nginx-proxy-manager\n\nVOLUME [ \"/data\" ]\nENTRYPOINT [ \"/init\" ]\n\nLABEL org.label-schema.schema-version=\"1.0\" \\\n\torg.label-schema.license=\"MIT\" \\\n\torg.label-schema.name=\"nginx-proxy-manager\" \\\n\torg.label-schema.description=\"Docker container for managing Nginx proxy hosts with a simple, powerful interface \" \\\n\torg.label-schema.url=\"https://github.com/jc21/nginx-proxy-manager\" \\\n\torg.label-schema.vcs-url=\"https://github.com/jc21/nginx-proxy-manager.git\" \\\n\torg.label-schema.cmd=\"docker run --rm -ti jc21/nginx-proxy-manager:latest\"\n"
  },
  {
    "path": "docker/ci.env",
    "content": "AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0\nAUTHENTIK_REDIS__HOST=authentik-redis\nAUTHENTIK_POSTGRESQL__HOST=pgdb.internal\nAUTHENTIK_POSTGRESQL__USER=authentik\nAUTHENTIK_POSTGRESQL__NAME=authentik\nAUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj\nAUTHENTIK_BOOTSTRAP_PASSWORD=admin\nAUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com\n"
  },
  {
    "path": "docker/dev/Dockerfile",
    "content": "FROM nginxproxymanager/testca AS testca\nFROM nginxproxymanager/nginx-full:certbot-node\nLABEL maintainer=\"Jamie Curnow <jc@jc21.com>\"\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nENV SUPPRESS_NO_CONFIG_WARNING=1 \\\n\tS6_BEHAVIOUR_IF_STAGE2_FAILS=1 \\\n\tS6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \\\n\tS6_FIX_ATTRS_HIDDEN=1 \\\n\tS6_KILL_FINISH_MAXTIME=10000 \\\n\tS6_VERBOSITY=2 \\\n\tNODE_OPTIONS=\"--openssl-legacy-provider\"\n\nRUN echo \"fs.file-max = 65535\" > /etc/sysctl.conf \\\n\t&& apt-get update \\\n\t&& apt-get install -y jq python3-pip logrotate moreutils \\\n\t&& apt-get clean \\\n\t&& rm -rf /var/lib/apt/lists/*\n\n# Task\nWORKDIR /usr\nRUN curl -sL https://taskfile.dev/install.sh | sh\nWORKDIR /root\n\nCOPY rootfs /\nCOPY scripts/install-s6 /tmp/install-s6\nRUN rm -f /etc/nginx/conf.d/production.conf \\\n\t&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \\\n\t&& /tmp/install-s6 \"${TARGETPLATFORM}\" \\\n\t&& rm -f /tmp/install-s6 \\\n\t&& chmod 644 -R /root/.cache\n\n# Certs for testing purposes\nCOPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt\n\nEXPOSE 80 81 443\nENTRYPOINT [ \"/init\" ]\n"
  },
  {
    "path": "docker/dev/dnsrouter-config.json",
    "content": "{\n    \"log\": {\n        \"format\": \"nice\",\n        \"level\": \"debug\"\n    },\n    \"servers\": [\n        {\n            \"host\": \"0.0.0.0\",\n            \"port\": 53,\n            \"upstreams\": [\n                {\n                    \"regex\": \"website[0-9]+.example\\\\.com\",\n                    \"upstream\": \"127.0.0.11\"\n                },\n                {\n                    \"regex\": \".*\\\\.example\\\\.com\",\n                    \"upstream\": \"1.1.1.1\"\n                },\n                {\n                    \"regex\": \"local\",\n                    \"nxdomain\": true\n                }\n            ],\n            \"internal\": null,\n            \"default_upstream\": \"127.0.0.11\"\n        }\n    ]\n}\n"
  },
  {
    "path": "docker/dev/letsencrypt.ini",
    "content": "text = True\nnon-interactive = True\nwebroot-path = /data/letsencrypt-acme-challenge\nkey-type = ecdsa\nelliptic-curve = secp384r1\npreferred-chain = ISRG Root X1\nserver =\n"
  },
  {
    "path": "docker/dev/pdns-db.sql",
    "content": "/*\n\nHow this was generated:\n1. bring up an empty pdns stack\n2. use api to create a zone ...\n\ncurl -X POST \\\n  'http://npm.dev:8081/api/v1/servers/localhost/zones' \\\n  --header 'X-API-Key: npm' \\\n  --header 'Content-Type: application/json' \\\n  --data-raw '{\n  \"name\": \"example.com.\",\n  \"kind\": \"Native\",\n  \"masters\": [],\n  \"nameservers\": [\n    \"ns1.pdns.\",\n    \"ns2.pdns.\"\n  ]\n}'\n\n3. Dump sql:\n\ndocker exec -ti npm.pdns.db mysqldump -u pdns -p pdns\n\n*/\n\n----------------------------------------------------------------------\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `comments`\n--\n\nDROP TABLE IF EXISTS `comments`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `comments` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `domain_id` int(11) NOT NULL,\n  `name` varchar(255) NOT NULL,\n  `type` varchar(10) NOT NULL,\n  `modified_at` int(11) NOT NULL,\n  `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,\n  `comment` text CHARACTER SET utf8mb3 NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `comments_name_type_idx` (`name`,`type`),\n  KEY `comments_order_idx` (`domain_id`,`modified_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `comments`\n--\n\nLOCK TABLES `comments` WRITE;\n/*!40000 ALTER TABLE `comments` DISABLE KEYS */;\n/*!40000 ALTER TABLE `comments` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cryptokeys`\n--\n\nDROP TABLE IF EXISTS `cryptokeys`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cryptokeys` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `domain_id` int(11) NOT NULL,\n  `flags` int(11) NOT NULL,\n  `active` tinyint(1) DEFAULT NULL,\n  `published` tinyint(1) DEFAULT 1,\n  `content` text DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `domainidindex` (`domain_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cryptokeys`\n--\n\nLOCK TABLES `cryptokeys` WRITE;\n/*!40000 ALTER TABLE `cryptokeys` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cryptokeys` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domainmetadata`\n--\n\nDROP TABLE IF EXISTS `domainmetadata`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domainmetadata` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `domain_id` int(11) NOT NULL,\n  `kind` varchar(32) DEFAULT NULL,\n  `content` text DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `domainmetadata_idx` (`domain_id`,`kind`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domainmetadata`\n--\n\nLOCK TABLES `domainmetadata` WRITE;\n/*!40000 ALTER TABLE `domainmetadata` DISABLE KEYS */;\nINSERT INTO `domainmetadata` VALUES\n(1,1,'SOA-EDIT-API','DEFAULT');\n/*!40000 ALTER TABLE `domainmetadata` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domains`\n--\n\nDROP TABLE IF EXISTS `domains`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domains` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) NOT NULL,\n  `master` varchar(128) DEFAULT NULL,\n  `last_check` int(11) DEFAULT NULL,\n  `type` varchar(8) NOT NULL,\n  `notified_serial` int(10) unsigned DEFAULT NULL,\n  `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,\n  `options` varchar(64000) DEFAULT NULL,\n  `catalog` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name_index` (`name`),\n  KEY `catalog_idx` (`catalog`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domains`\n--\n\nLOCK TABLES `domains` WRITE;\n/*!40000 ALTER TABLE `domains` DISABLE KEYS */;\nINSERT INTO `domains` VALUES\n(1,'example.com','',NULL,'NATIVE',NULL,'',NULL,NULL);\n/*!40000 ALTER TABLE `domains` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `records`\n--\n\nDROP TABLE IF EXISTS `records`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `records` (\n  `id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `domain_id` int(11) DEFAULT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `type` varchar(10) DEFAULT NULL,\n  `content` varchar(64000) DEFAULT NULL,\n  `ttl` int(11) DEFAULT NULL,\n  `prio` int(11) DEFAULT NULL,\n  `disabled` tinyint(1) DEFAULT 0,\n  `ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,\n  `auth` tinyint(1) DEFAULT 1,\n  PRIMARY KEY (`id`),\n  KEY `nametype_index` (`name`,`type`),\n  KEY `domain_id` (`domain_id`),\n  KEY `ordername` (`ordername`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `records`\n--\n\nLOCK TABLES `records` WRITE;\n/*!40000 ALTER TABLE `records` DISABLE KEYS */;\nINSERT INTO `records` VALUES\n(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1),\n(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1),\n(3,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2023030501 10800 3600 604800 3600',1500,0,0,NULL,1);\n/*!40000 ALTER TABLE `records` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `supermasters`\n--\n\nDROP TABLE IF EXISTS `supermasters`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `supermasters` (\n  `ip` varchar(64) NOT NULL,\n  `nameserver` varchar(255) NOT NULL,\n  `account` varchar(40) CHARACTER SET utf8mb3 NOT NULL,\n  PRIMARY KEY (`ip`,`nameserver`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `supermasters`\n--\n\nLOCK TABLES `supermasters` WRITE;\n/*!40000 ALTER TABLE `supermasters` DISABLE KEYS */;\n/*!40000 ALTER TABLE `supermasters` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `tsigkeys`\n--\n\nDROP TABLE IF EXISTS `tsigkeys`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `tsigkeys` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) DEFAULT NULL,\n  `algorithm` varchar(50) DEFAULT NULL,\n  `secret` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `namealgoindex` (`name`,`algorithm`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `tsigkeys`\n--\n\nLOCK TABLES `tsigkeys` WRITE;\n/*!40000 ALTER TABLE `tsigkeys` DISABLE KEYS */;\n/*!40000 ALTER TABLE `tsigkeys` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n"
  },
  {
    "path": "docker/dev/squid.conf",
    "content": "#\tWELCOME TO SQUID 6.6\n#\t----------------------------\n#\n#\tThis is the documentation for the Squid configuration file.\n#\tThis documentation can also be found online at:\n#\t\thttp://www.squid-cache.org/Doc/config/\n#\n#\tYou may wish to look at the Squid home page and wiki for the\n#\tFAQ and other documentation:\n#\t\thttp://www.squid-cache.org/\n#\t\thttps://wiki.squid-cache.org/SquidFaq\n#\t\thttps://wiki.squid-cache.org/ConfigExamples\n#\n\n# Example rule allowing access from your local networks.\n# Adapt to list your (internal) IP networks from where browsing\n# should be allowed\nacl localnet src 0.0.0.1-0.255.255.255\t# RFC 1122 \"this\" network (LAN)\nacl localnet src 10.0.0.0/8\t\t# RFC 1918 local private network (LAN)\nacl localnet src 100.64.0.0/10\t\t# RFC 6598 shared address space (CGN)\nacl localnet src 169.254.0.0/16 \t# RFC 3927 link-local (directly plugged) machines\nacl localnet src 172.0.0.0/8\nacl localnet src 192.168.0.0/16\t\t# RFC 1918 local private network (LAN)\nacl localnet src fc00::/7       \t# RFC 4193 local private network range\nacl localnet src fe80::/10      \t# RFC 4291 link-local (directly plugged) machines\n\nacl SSL_ports port 443\nacl Safe_ports port 80\t\t# http\nacl Safe_ports port 81\nacl Safe_ports port 443\t\t# https\n\n#\n# Recommended minimum Access Permission configuration:\n#\n# Deny requests to certain unsafe ports\nhttp_access deny !Safe_ports\n\n# Deny CONNECT to other than secure SSL ports\nhttp_access deny CONNECT !SSL_ports\n\n# Only allow cachemgr access from localhost\nhttp_access allow localhost manager\nhttp_access deny manager\n\n# This default configuration only allows localhost requests because a more\n# permissive Squid installation could introduce new attack vectors into the\n# network by proxying external TCP connections to unprotected services.\nhttp_access allow localhost\n\n# The two deny rules below are unnecessary in this default configuration\n# because they are followed by a \"deny all\" rule. However, they may become\n# critically important when you start allowing external requests below them.\n\n# Protect web applications running on the same server as Squid. They often\n# assume that only local users can access them at \"localhost\" ports.\nhttp_access deny to_localhost\n\n# Protect cloud servers that provide local users with sensitive info about\n# their server via certain well-known link-local (a.k.a. APIPA) addresses.\nhttp_access deny to_linklocal\n\n#\n# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS\n#\ninclude /etc/squid/conf.d/*.conf\n\n# For example, to allow access from your local networks, you may uncomment the\n# following rule (and/or add rules that match your definition of \"local\"):\n# http_access allow localnet\n\n# And finally deny all other access to this proxy\nhttp_access deny all\n\n# Squid normally listens to port 3128\nhttp_port 3128\n\n# Leave coredumps in the first cache dir\ncoredump_dir /var/spool/squid\n\n#\n# Add any of your own refresh_pattern entries above these.\n#\nrefresh_pattern ^ftp:\t\t1440\t20%\t10080\nrefresh_pattern -i (/cgi-bin/|\\?) 0\t0%\t0\nrefresh_pattern \\/(Packages|Sources)(|\\.bz2|\\.gz|\\.xz)$ 0 0% 0 refresh-ims\nrefresh_pattern \\/Release(|\\.gpg)$ 0 0% 0 refresh-ims\nrefresh_pattern \\/InRelease$ 0 0% 0 refresh-ims\nrefresh_pattern \\/(Translation-.*)(|\\.bz2|\\.gz|\\.xz)$ 0 0% 0 refresh-ims\n# example pattern for deb packages\n#refresh_pattern (\\.deb|\\.udeb)$   129600 100% 129600\nrefresh_pattern .\t\t0\t20%\t4320\n\n"
  },
  {
    "path": "docker/docker-compose.ci.mysql.yml",
    "content": "# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.\nservices:\n\n  fullstack:\n    environment:\n      DB_MYSQL_HOST: 'db-mysql'\n      DB_MYSQL_PORT: '3306'\n      DB_MYSQL_USER: 'npm'\n      DB_MYSQL_PASSWORD: 'npmpass'\n      DB_MYSQL_NAME: 'npm'\n    depends_on:\n      - db-mysql\n\n  db-mysql:\n    image: jc21/mariadb-aria\n    environment:\n      MYSQL_ROOT_PASSWORD: 'npm'\n      MYSQL_DATABASE: 'npm'\n      MYSQL_USER: 'npm'\n      MYSQL_PASSWORD: 'npmpass'\n      MARIADB_AUTO_UPGRADE: '1'\n    volumes:\n      - mysql_vol:/var/lib/mysql\n    networks:\n      - fulltest\n\nvolumes:\n  mysql_vol:\n"
  },
  {
    "path": "docker/docker-compose.ci.postgres.yml",
    "content": "# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.\nservices:\n  cypress:\n    environment:\n      CYPRESS_stack: \"postgres\"\n\n  fullstack:\n    environment:\n      DB_POSTGRES_HOST: \"pgdb.internal\"\n      DB_POSTGRES_PORT: \"5432\"\n      DB_POSTGRES_USER: \"npm\"\n      DB_POSTGRES_PASSWORD: \"npmpass\"\n      DB_POSTGRES_NAME: \"npm\"\n    depends_on:\n      - db-postgres\n      - authentik\n      - authentik-worker\n      - authentik-ldap\n\n  db-postgres:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: \"npm\"\n      POSTGRES_PASSWORD: \"npmpass\"\n      POSTGRES_DB: \"npm\"\n    volumes:\n      - psql_vol:/var/lib/postgresql/data\n      - ./ci/postgres:/docker-entrypoint-initdb.d\n    networks:\n      fulltest:\n        aliases:\n          - pgdb.internal\n\n  authentik-redis:\n    image: \"redis:alpine\"\n    command: --save 60 1 --loglevel warning\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"redis-cli ping | grep PONG\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 3s\n    volumes:\n      - redis_vol:/data\n    networks:\n      - fulltest\n\n  authentik:\n    image: ghcr.io/goauthentik/server:2024.10.1\n    restart: unless-stopped\n    command: server\n    env_file:\n      - ci.env\n    depends_on:\n      - authentik-redis\n      - db-postgres\n    networks:\n      - fulltest\n\n  authentik-worker:\n    image: ghcr.io/goauthentik/server:2024.10.1\n    restart: unless-stopped\n    command: worker\n    env_file:\n      - ci.env\n    depends_on:\n      - authentik-redis\n      - db-postgres\n    networks:\n      - fulltest\n\n  authentik-ldap:\n    image: ghcr.io/goauthentik/ldap:2024.10.1\n    environment:\n      AUTHENTIK_HOST: \"http://authentik:9000\"\n      AUTHENTIK_INSECURE: \"true\"\n      AUTHENTIK_TOKEN: \"wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp\"\n    restart: unless-stopped\n    depends_on:\n      - authentik\n    networks:\n      - fulltest\n\nvolumes:\n  psql_vol:\n  redis_vol:\n"
  },
  {
    "path": "docker/docker-compose.ci.sqlite.yml",
    "content": "# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.\nservices:\n\n  fullstack:\n    environment:\n      DB_SQLITE_FILE: '/data/mydb.sqlite'\n      PUID: 1000\n      PGID: 1000\n      DISABLE_IPV6: 'true'\n"
  },
  {
    "path": "docker/docker-compose.ci.yml",
    "content": "# WARNING: This is a CI docker-compose file used for building\n# and testing of the entire app, it should not be used for production.\n# This is a base compose file, it should be extended with a\n# docker-compose.ci.*.yml file\nservices:\n  fullstack:\n    image: \"${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}\"\n    environment:\n      TZ: \"${TZ:-Australia/Brisbane}\"\n      DEBUG: \"true\"\n      CI: \"true\"\n      FORCE_COLOR: 1\n      # Required for DNS Certificate provisioning in CI\n      LE_SERVER: \"https://ca.internal/acme/acme/directory\"\n      REQUESTS_CA_BUNDLE: \"/etc/ssl/certs/NginxProxyManager.crt\"\n    volumes:\n      - \"npm_data_ci:/data\"\n      - \"npm_le_ci:/etc/letsencrypt\"\n      - \"./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro\"\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    healthcheck:\n      test: [\"CMD\", \"/usr/bin/check-health\"]\n      interval: 10s\n      timeout: 3s\n    expose:\n      - \"80/tcp\"\n      - \"81/tcp\"\n      - \"443/tcp\"\n      - \"1500/tcp\"\n      - \"1501/tcp\"\n      - \"1502/tcp\"\n      - \"1503/tcp\"\n    networks:\n      fulltest:\n        aliases:\n          - website1.example.com\n          - website2.example.com\n          - website3.example.com\n\n  stepca:\n    image: jc21/testca\n    volumes:\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    networks:\n      fulltest:\n        aliases:\n          - ca.internal\n\n  pdns:\n    image: pschiffe/pdns-mysql:4.8\n    volumes:\n      - \"/etc/localtime:/etc/localtime:ro\"\n    environment:\n      PDNS_master: \"yes\"\n      PDNS_api: \"yes\"\n      PDNS_api_key: \"npm\"\n      PDNS_webserver: \"yes\"\n      PDNS_webserver_address: \"0.0.0.0\"\n      PDNS_webserver_password: \"npm\"\n      PDNS_webserver-allow-from: \"127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8\"\n      PDNS_version_string: \"anonymous\"\n      PDNS_default_ttl: 1500\n      PDNS_allow_axfr_ips: \"127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8\"\n      PDNS_gmysql_host: pdns-db\n      PDNS_gmysql_port: 3306\n      PDNS_gmysql_user: pdns\n      PDNS_gmysql_password: pdns\n      PDNS_gmysql_dbname: pdns\n    depends_on:\n      - pdns-db\n    networks:\n      fulltest:\n        aliases:\n          - ns1.pdns\n          - ns2.pdns\n\n  pdns-db:\n    image: mariadb\n    environment:\n      MYSQL_ROOT_PASSWORD: \"pdns\"\n      MYSQL_DATABASE: \"pdns\"\n      MYSQL_USER: \"pdns\"\n      MYSQL_PASSWORD: \"pdns\"\n    volumes:\n      - \"pdns_mysql_vol:/var/lib/mysql\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n      - \"./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro\"\n    networks:\n      - fulltest\n\n  dnsrouter:\n    image: jc21/dnsrouter\n    volumes:\n      - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro\n    networks:\n      - fulltest\n\n  cypress:\n    image: \"${IMAGE}-cypress:ci-${BUILD_NUMBER}\"\n    build:\n      context: ../\n      dockerfile: test/cypress/Dockerfile\n    environment:\n      HTTP_PROXY: \"squid:3128\"\n      HTTPS_PROXY: \"squid:3128\"\n    volumes:\n      - \"cypress_logs:/test/results\"\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    command: cypress run --browser chrome --config-file=cypress/config/ci.mjs\n    networks:\n      - fulltest\n\n  squid:\n    image: ubuntu/squid\n    volumes:\n      - \"./dev/squid.conf:/etc/squid/squid.conf:ro\"\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    networks:\n      - fulltest\n\nvolumes:\n  cypress_logs:\n  npm_data_ci:\n  npm_le_ci:\n  pdns_mysql_vol:\n\nnetworks:\n  fulltest:\n    name: \"npm-${BRANCH_LOWER}-ci-${BUILD_NUMBER}\"\n"
  },
  {
    "path": "docker/docker-compose.dev.yml",
    "content": "# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.\nservices:\n  fullstack:\n    image: npm2dev:core\n    container_name: npm2dev.core\n    build:\n      context: ./\n      dockerfile: ./dev/Dockerfile\n    ports:\n      - 3080:80\n      - 3081:81\n      - 3443:443\n    networks:\n      nginx_proxy_manager:\n        aliases:\n          - website1.example.com\n          - website2.example.com\n          - website3.example.com\n    environment:\n      TZ: \"${TZ:-Australia/Brisbane}\"\n      PUID: 1000\n      PGID: 1000\n      FORCE_COLOR: 1\n      # specifically for dev:\n      DEBUG: \"true\"\n      DEVELOPMENT: \"true\"\n      LE_STAGING: \"true\"\n      # db:\n      # DB_MYSQL_HOST: 'db'\n      # DB_MYSQL_PORT: '3306'\n      # DB_MYSQL_USER: 'npm'\n      # DB_MYSQL_PASSWORD: 'npm'\n      # DB_MYSQL_NAME: 'npm'\n      # db-postgres:\n      DB_POSTGRES_HOST: \"pgdb.internal\"\n      DB_POSTGRES_PORT: \"5432\"\n      DB_POSTGRES_USER: \"npm\"\n      DB_POSTGRES_PASSWORD: \"npmpass\"\n      DB_POSTGRES_NAME: \"npm\"\n      # DB_SQLITE_FILE: \"/data/database.sqlite\"\n      # DISABLE_IPV6: \"true\"\n      # Required for DNS Certificate provisioning testing:\n      LE_SERVER: \"https://ca.internal/acme/acme/directory\"\n      REQUESTS_CA_BUNDLE: \"/etc/ssl/certs/NginxProxyManager.crt\"\n    volumes:\n      - npm_data:/data\n      - le_data:/etc/letsencrypt\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - ../backend:/app\n      - ../frontend:/frontend\n      - \"/etc/localtime:/etc/localtime:ro\"\n    healthcheck:\n      test: [\"CMD\", \"/usr/bin/check-health\"]\n      interval: 10s\n      timeout: 3s\n    depends_on:\n      - db\n      - db-postgres\n      - authentik\n      - authentik-worker\n      - authentik-ldap\n    working_dir: /app\n\n  db:\n    image: jc21/mariadb-aria\n    container_name: npm2dev.db\n    ports:\n      - 33306:3306\n    networks:\n      - nginx_proxy_manager\n    environment:\n      TZ: \"${TZ:-Australia/Brisbane}\"\n      MYSQL_ROOT_PASSWORD: \"npm\"\n      MYSQL_DATABASE: \"npm\"\n      MYSQL_USER: \"npm\"\n      MYSQL_PASSWORD: \"npm\"\n    volumes:\n      - db_data:/var/lib/mysql\n      - \"/etc/localtime:/etc/localtime:ro\"\n\n  db-postgres:\n    image: postgres:17\n    container_name: npm2dev.db-postgres\n    environment:\n      POSTGRES_USER: \"npm\"\n      POSTGRES_PASSWORD: \"npmpass\"\n      POSTGRES_DB: \"npm\"\n    volumes:\n      - psql_data:/var/lib/postgresql/data\n      - ./ci/postgres:/docker-entrypoint-initdb.d\n    networks:\n      nginx_proxy_manager:\n        aliases:\n          - pgdb.internal\n\n  stepca:\n    image: jc21/testca\n    container_name: npm2dev.stepca\n    volumes:\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    networks:\n      nginx_proxy_manager:\n        aliases:\n          - ca.internal\n\n  dnsrouter:\n    image: jc21/dnsrouter\n    container_name: npm2dev.dnsrouter\n    volumes:\n      - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro\n    networks:\n      - nginx_proxy_manager\n\n  swagger:\n    image: swaggerapi/swagger-ui:latest\n    container_name: npm2dev.swagger\n    ports:\n      - 3082:80\n    environment:\n      URL: \"http://npm:81/api/schema\"\n      PORT: \"80\"\n    depends_on:\n      - fullstack\n\n  squid:\n    image: ubuntu/squid\n    container_name: npm2dev.squid\n    volumes:\n      - \"./dev/squid.conf:/etc/squid/squid.conf:ro\"\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    networks:\n      - nginx_proxy_manager\n    ports:\n      - 8128:3128\n\n  pdns:\n    image: pschiffe/pdns-mysql:4.8\n    container_name: npm2dev.pdns\n    volumes:\n      - \"/etc/localtime:/etc/localtime:ro\"\n    environment:\n      PDNS_master: \"yes\"\n      PDNS_api: \"yes\"\n      PDNS_api_key: \"npm\"\n      PDNS_webserver: \"yes\"\n      PDNS_webserver_address: \"0.0.0.0\"\n      PDNS_webserver_password: \"npm\"\n      PDNS_webserver-allow-from: \"127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8\"\n      PDNS_version_string: \"anonymous\"\n      PDNS_default_ttl: 1500\n      PDNS_allow_axfr_ips: \"127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8\"\n      PDNS_gmysql_host: pdns-db\n      PDNS_gmysql_port: 3306\n      PDNS_gmysql_user: pdns\n      PDNS_gmysql_password: pdns\n      PDNS_gmysql_dbname: pdns\n    depends_on:\n      - pdns-db\n    networks:\n      nginx_proxy_manager:\n        aliases:\n          - ns1.pdns\n          - ns2.pdns\n\n  pdns-db:\n    image: mariadb\n    container_name: npm2dev.pdns-db\n    environment:\n      MYSQL_ROOT_PASSWORD: \"pdns\"\n      MYSQL_DATABASE: \"pdns\"\n      MYSQL_USER: \"pdns\"\n      MYSQL_PASSWORD: \"pdns\"\n    volumes:\n      - \"pdns_mysql:/var/lib/mysql\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n      - \"./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro\"\n    networks:\n      - nginx_proxy_manager\n\n  cypress:\n    image: npm2dev:cypress\n    container_name: npm2dev.cypress\n    build:\n      context: ../\n      dockerfile: test/cypress/Dockerfile\n    environment:\n      HTTP_PROXY: \"squid:3128\"\n      HTTPS_PROXY: \"squid:3128\"\n    volumes:\n      - \"../test/results:/results\"\n      - \"./dev/resolv.conf:/etc/resolv.conf:ro\"\n      - \"/etc/localtime:/etc/localtime:ro\"\n    command: cypress run --browser chrome --config-file=cypress/config/ci.mjs\n    networks:\n      - nginx_proxy_manager\n\n  authentik-redis:\n    image: \"redis:alpine\"\n    container_name: npm2dev.authentik-redis\n    command: --save 60 1 --loglevel warning\n    networks:\n      - nginx_proxy_manager\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"redis-cli ping | grep PONG\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 3s\n    volumes:\n      - redis_data:/data\n\n  authentik:\n    image: ghcr.io/goauthentik/server:2024.10.1\n    container_name: npm2dev.authentik\n    restart: unless-stopped\n    command: server\n    networks:\n      - nginx_proxy_manager\n    env_file:\n      - ci.env\n    ports:\n      - 9000:9000\n    depends_on:\n      - authentik-redis\n      - db-postgres\n\n  authentik-worker:\n    image: ghcr.io/goauthentik/server:2024.10.1\n    container_name: npm2dev.authentik-worker\n    restart: unless-stopped\n    command: worker\n    networks:\n      - nginx_proxy_manager\n    env_file:\n      - ci.env\n    depends_on:\n      - authentik-redis\n      - db-postgres\n\n  authentik-ldap:\n    image: ghcr.io/goauthentik/ldap:2024.10.1\n    container_name: npm2dev.authentik-ldap\n    networks:\n      - nginx_proxy_manager\n    environment:\n      AUTHENTIK_HOST: \"http://authentik:9000\"\n      AUTHENTIK_INSECURE: \"true\"\n      AUTHENTIK_TOKEN: \"wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp\"\n    restart: unless-stopped\n    depends_on:\n      - authentik\n\nvolumes:\n  npm_data:\n    name: npm2dev_core_data\n  le_data:\n    name: npm2dev_le_data\n  db_data:\n    name: npm2dev_db_data\n  pdns_mysql:\n    name: npnpm2dev_pdns_mysql\n  psql_data:\n    name: npm2dev_psql_data\n  redis_data:\n    name: npm2dev_redis_data\n\nnetworks:\n  nginx_proxy_manager:\n    name: npm2dev_network\n"
  },
  {
    "path": "docker/rootfs/etc/letsencrypt.ini",
    "content": "text = True\nnon-interactive = True\nwebroot-path = /data/letsencrypt-acme-challenge\nkey-type = ecdsa\nelliptic-curve = secp384r1\npreferred-chain = ISRG Root X1\n"
  },
  {
    "path": "docker/rootfs/etc/logrotate.d/nginx-proxy-manager",
    "content": "/data/logs/*_access.log /data/logs/*/access.log {\n    su npm npm\n    create 0644\n    weekly\n    rotate 4\n    missingok\n    notifempty\n    compress\n    sharedscripts\n    postrotate\n    kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true\n    endscript\n}\n\n/data/logs/*_error.log /data/logs/*/error.log {\n    su npm npm\n    create 0644\n    weekly\n    rotate 10\n    missingok\n    notifempty\n    compress\n    sharedscripts\n    postrotate\n    kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true\n    endscript\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/default.conf",
    "content": "# \"You are not configured\" page, which is the default if another default doesn't exist\nserver {\n\tlisten 80;\n\tlisten [::]:80;\n\n\tset $forward_scheme \"http\";\n\tset $server \"127.0.0.1\";\n\tset $port \"80\";\n\n\tserver_name localhost-nginx-proxy-manager;\n\taccess_log /data/logs/fallback_http_access.log standard;\n\terror_log /data/logs/fallback_http_error.log warn;\n\tinclude conf.d/include/assets.conf;\n\tinclude conf.d/include/block-exploits.conf;\n\tinclude conf.d/include/letsencrypt-acme-challenge.conf;\n\n\tlocation / {\n\t\tindex index.html;\n\t\troot /var/www/html;\n\t}\n}\n\n# First 443 Host, which is the default if another default doesn't exist\nserver {\n\tlisten 443 ssl;\n\tlisten [::]:443 ssl;\n\n\tset $forward_scheme \"https\";\n\tset $server \"127.0.0.1\";\n\tset $port \"443\";\n\n\tserver_name localhost;\n\taccess_log /data/logs/fallback_http_access.log standard;\n\terror_log /dev/null crit;\n\tinclude conf.d/include/ssl-ciphers.conf;\n\tssl_reject_handshake on;\n\n\treturn 444;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/dev.conf",
    "content": "server {\n\tlisten 81 default;\n\tlisten [::]:81 default;\n\n\tserver_name nginxproxymanager-dev;\n\troot /app/frontend/dist;\n\taccess_log /dev/null;\n\n\tlocation /api {\n\t\treturn 302 /api/;\n\t}\n\n\tlocation /api/ {\n\t\tadd_header            X-Served-By $host;\n\t\tproxy_http_version    1.1;\n\t\tproxy_set_header Host $host;\n\t\tproxy_set_header      X-Forwarded-Scheme $scheme;\n\t\tproxy_set_header      X-Forwarded-Proto  $scheme;\n\t\tproxy_set_header      X-Forwarded-For    $remote_addr;\n\t\tproxy_pass            http://127.0.0.1:3000/;\n\n\t\tproxy_read_timeout 15m;\n\t\tproxy_send_timeout 15m;\n\t}\n\n\tlocation / {\n\t\tadd_header            X-Served-By $host;\n\t\tproxy_http_version    1.1;\n\t\tproxy_set_header Host $host;\n\t\tproxy_set_header      Upgrade $http_upgrade;\n\t\tproxy_set_header      Connection \"Upgrade\";\n\t\tproxy_set_header      X-Forwarded-Scheme $scheme;\n\t\tproxy_set_header      X-Forwarded-Proto  $scheme;\n\t\tproxy_set_header      X-Forwarded-For    $remote_addr;\n\t\tproxy_pass            http://127.0.0.1:5173;\n\t}\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/.gitignore",
    "content": "resolvers.conf\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/assets.conf",
    "content": "location ~* ^.*\\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\\.map|js\\.map)$ {\n\tif_modified_since off;\n\n\t# use the public cache\n\tproxy_cache public-cache;\n\tproxy_cache_key $host$request_uri;\n\n\t# ignore these headers for media\n\tproxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires;\n\n\t# cache 200s and also 404s (not ideal but there are a few 404 images for some reason)\n\tproxy_cache_valid any 30m;\n\tproxy_cache_valid 404 1m;\n\n\t# strip this header to avoid If-Modified-Since requests\n\tproxy_hide_header Last-Modified;\n\tproxy_hide_header Cache-Control;\n\tproxy_hide_header Vary;\n\n\tproxy_cache_bypass 0;\n\tproxy_no_cache 0;\n\n\tproxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404;\n\tproxy_connect_timeout 5s;\n\tproxy_read_timeout 45s;\n\n\texpires @30m;\n\taccess_log  off;\n\n\tinclude conf.d/include/proxy.conf;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf",
    "content": "## Block SQL injections\nset $block_sql_injections 0;\n\nif ($query_string ~ \"union.*select.*\\(\") {\n\tset $block_sql_injections 1;\n}\n\nif ($query_string ~ \"union.*all.*select.*\") {\n\tset $block_sql_injections 1;\n}\n\nif ($query_string ~ \"concat.*\\(\") {\n\tset $block_sql_injections 1;\n}\n\nif ($block_sql_injections = 1) {\n\treturn 403;\n}\n\n## Block file injections\nset $block_file_injections 0;\n\nif ($query_string ~ \"[a-zA-Z0-9_]=http://\") {\n\tset $block_file_injections 1;\n}\n\nif ($query_string ~ \"[a-zA-Z0-9_]=(\\.\\.//?)+\") {\n\tset $block_file_injections 1;\n}\n\nif ($query_string ~ \"[a-zA-Z0-9_]=/([a-z0-9_.]//?)+\") {\n\tset $block_file_injections 1;\n}\n\nif ($block_file_injections = 1) {\n\treturn 403;\n}\n\n## Block common exploits\nset $block_common_exploits 0;\n\nif ($query_string ~ \"(<|%3C).*script.*(>|%3E)\") {\n\tset $block_common_exploits 1;\n}\n\nif ($query_string ~ \"GLOBALS(=|\\[|\\%[0-9A-Z]{0,2})\") {\n\tset $block_common_exploits 1;\n}\n\nif ($query_string ~ \"_REQUEST(=|\\[|\\%[0-9A-Z]{0,2})\") {\n\tset $block_common_exploits 1;\n}\n\nif ($query_string ~ \"proc/self/environ\") {\n\tset $block_common_exploits 1;\n}\n\nif ($query_string ~ \"mosConfig_[a-zA-Z_]{1,21}(=|\\%3D)\") {\n\tset $block_common_exploits 1;\n}\n\nif ($query_string ~ \"base64_(en|de)code\\(.*\\)\") {\n\tset $block_common_exploits 1;\n}\n\nif ($block_common_exploits = 1) {\n\treturn 403;\n}\n\n## Block spam\nset $block_spam 0;\n\nif ($query_string ~ \"\\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\\b\") {\n\tset $block_spam 1;\n}\n\nif ($query_string ~ \"\\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\\b\") {\n\tset $block_spam 1;\n}\n\nif ($query_string ~ \"\\b(ambien|blue\\spill|cialis|cocaine|ejaculation|erectile)\\b\") {\n\tset $block_spam 1;\n}\n\nif ($query_string ~ \"\\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\\b\") {\n\tset $block_spam 1;\n}\n\nif ($block_spam = 1) {\n\treturn 403;\n}\n\n## Block user agents\nset $block_user_agents 0;\n\n# Disable Akeeba Remote Control 2.5 and earlier\nif ($http_user_agent ~ \"Indy Library\") {\n\tset $block_user_agents 1;\n}\n\n# Common bandwidth hoggers and hacking tools.\nif ($http_user_agent ~ \"libwww-perl\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"GetRight\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"GetWeb!\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"Go!Zilla\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"Download Demon\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"Go-Ahead-Got-It\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"TurnitinBot\") {\n\tset $block_user_agents 1;\n}\n\nif ($http_user_agent ~ \"GrabNet\") {\n\tset $block_user_agents 1;\n}\n\nif ($block_user_agents = 1) {\n\treturn 403;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf",
    "content": "set $test \"\";\nif ($scheme = \"http\") {\n\tset $test \"H\";\n}\nif ($request_uri = /.well-known/acme-challenge/test-challenge) {\n\tset $test \"${test}T\";\n}\n\n# Check if the ssl staff has been handled\nset $test_ssl_handled \"\";\nif ($trust_forwarded_proto = \"\") {\n\tset $trust_forwarded_proto \"F\";\n}\nif ($trust_forwarded_proto = \"T\") {\n\tset $test_ssl_handled \"${test_ssl_handled}T\";\n}\nif ($http_x_forwarded_proto = \"https\") {\n\tset $test_ssl_handled \"${test_ssl_handled}S\";\n}\nif ($http_x_forwarded_scheme = \"https\") {\n\tset $test_ssl_handled \"${test_ssl_handled}S\";\n}\nif ($test_ssl_handled = \"TSS\") {\n\tset $test_ssl_handled \"TS\";\n}\nif ($test_ssl_handled = \"TS\") {\n\tset $test \"${test}S\";\n}\n\nif ($test = H) {\n\treturn 301 https://$host$request_uri;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf",
    "content": "# This should be left blank is it is populated programatically\n# by the application backend.\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf",
    "content": "# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)\n# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel\n# other regex checks, because in our other config files have regex rule that denies access to files with dotted names.\nlocation ^~ /.well-known/acme-challenge/ {\n\t# Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure\n\t# we need to open up access by turning off auth and IP ACL for this location.\n\tauth_basic off;\n\tauth_request off;\n\tallow all;\n\n\t# Set correct content type. According to this:\n\t# https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29\n\t# Current specification requires \"text/plain\" or no content header at all.\n\t# It seems that \"text/plain\" is a safe option.\n\tdefault_type \"text/plain\";\n\n\t# This directory must be the same as in /etc/letsencrypt/cli.ini\n\t# as \"webroot-path\" parameter. Also don't forget to set \"authenticator\" parameter\n\t# there to \"webroot\".\n\t# Do NOT use alias, use root! Target directory is located here:\n\t# /var/www/common/letsencrypt/.well-known/acme-challenge/\n\troot /data/letsencrypt-acme-challenge;\n}\n\n# Hide /acme-challenge subdirectory and return 404 on all requests.\n# It is somewhat more secure than letting Nginx return 403.\n# Ending slash is important!\nlocation = /.well-known/acme-challenge/ {\n\treturn 404;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/log-proxy.conf",
    "content": "log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host \"$request_uri\" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] \"$http_user_agent\" \"$http_referer\"';\nlog_format standard '[$time_local] $status - $request_method $scheme $host \"$request_uri\" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] \"$http_user_agent\" \"$http_referer\"';\n\naccess_log /data/logs/fallback_http_access.log proxy;\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/log-stream.conf",
    "content": "log_format stream '[$time_local] [Client $remote_addr:$remote_port] $protocol $status $bytes_sent $bytes_received $session_time [Sent-to $upstream_addr] [Sent $upstream_bytes_sent] [Received $upstream_bytes_received] [Time $upstream_connect_time] $ssl_protocol $ssl_cipher';\n\naccess_log /data/logs/fallback_stream_access.log stream;\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/proxy.conf",
    "content": "add_header       X-Served-By $host;\nproxy_set_header Host $host;\nproxy_set_header X-Forwarded-Scheme $x_forwarded_scheme;\nproxy_set_header X-Forwarded-Proto  $x_forwarded_proto;\nproxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;\nproxy_set_header X-Real-IP          $remote_addr;\nproxy_pass       $forward_scheme://$server:$port$request_uri;\n\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf",
    "content": "ssl_session_timeout 5m;\nssl_session_cache shared:SSL_stream:50m;\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf",
    "content": "ssl_session_timeout 5m;\nssl_session_cache shared:SSL:50m;\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf",
    "content": "# intermediate configuration. tweak to your needs.\nssl_protocols TLSv1.2 TLSv1.3;\nssl_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';\nssl_prefer_server_ciphers off;\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/conf.d/production.conf",
    "content": "# Admin Interface\nserver {\n\tlisten 81 default;\n\tlisten [::]:81 default;\n\n\tserver_name nginxproxymanager;\n\troot /app/frontend;\n\taccess_log /dev/null;\n\n\tlocation /api {\n\t\treturn 302 /api/;\n\t}\n\n\tlocation /api/ {\n\t\tadd_header            X-Served-By $host;\n\t\tproxy_set_header Host $host;\n\t\tproxy_set_header      X-Forwarded-Scheme $scheme;\n\t\tproxy_set_header      X-Forwarded-Proto  $scheme;\n\t\tproxy_set_header      X-Forwarded-For    $remote_addr;\n\t\tproxy_pass            http://127.0.0.1:3000/;\n\n\t\tproxy_read_timeout 15m;\n\t\tproxy_send_timeout 15m;\n\t}\n\n\tlocation / {\n\t\tindex index.html;\n\t\tif ($request_uri ~ ^/(.*)\\.html$) {\n\t\t\treturn 302 /$1;\n\t\t}\n\t\ttry_files $uri $uri.html $uri/ /index.html;\n\t}\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/mime.types",
    "content": "types {\n    text/html                                        html htm shtml;\n    text/css                                         css;\n    text/xml                                         xml;\n    image/gif                                        gif;\n    image/jpeg                                       jpeg jpg;\n    application/javascript                           js;\n    application/atom+xml                             atom;\n    application/rss+xml                              rss;\n\n    text/mathml                                      mml;\n    text/plain                                       txt;\n    text/vnd.sun.j2me.app-descriptor                 jad;\n    text/vnd.wap.wml                                 wml;\n    text/x-component                                 htc;\n\n    image/png                                        png;\n    image/svg+xml                                    svg svgz;\n    image/tiff                                       tif tiff;\n    image/vnd.wap.wbmp                               wbmp;\n    image/webp                                       webp;\n    image/x-icon                                     ico;\n    image/x-jng                                      jng;\n    image/x-ms-bmp                                   bmp;\n\n    font/woff                                        woff;\n    font/woff2                                       woff2;\n\n    application/java-archive                         jar war ear;\n    application/json                                 json;\n    application/mac-binhex40                         hqx;\n    application/msword                               doc;\n    application/pdf                                  pdf;\n    application/postscript                           ps eps ai;\n    application/rtf                                  rtf;\n    application/vnd.apple.mpegurl                    m3u8;\n    application/vnd.google-earth.kml+xml             kml;\n    application/vnd.google-earth.kmz                 kmz;\n    application/vnd.ms-excel                         xls;\n    application/vnd.ms-fontobject                    eot;\n    application/vnd.ms-powerpoint                    ppt;\n    application/vnd.oasis.opendocument.graphics      odg;\n    application/vnd.oasis.opendocument.presentation  odp;\n    application/vnd.oasis.opendocument.spreadsheet   ods;\n    application/vnd.oasis.opendocument.text          odt;\n    application/vnd.openxmlformats-officedocument.presentationml.presentation\n                                                     pptx;\n    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\n                                                     xlsx;\n    application/vnd.openxmlformats-officedocument.wordprocessingml.document\n                                                     docx;\n    application/vnd.wap.wmlc                         wmlc;\n    application/x-7z-compressed                      7z;\n    application/x-cocoa                              cco;\n    application/x-java-archive-diff                  jardiff;\n    application/x-java-jnlp-file                     jnlp;\n    application/x-makeself                           run;\n    application/x-perl                               pl pm;\n    application/x-pilot                              prc pdb;\n    application/x-rar-compressed                     rar;\n    application/x-redhat-package-manager             rpm;\n    application/x-sea                                sea;\n    application/x-shockwave-flash                    swf;\n    application/x-stuffit                            sit;\n    application/x-tcl                                tcl tk;\n    application/x-x509-ca-cert                       der pem crt;\n    application/x-xpinstall                          xpi;\n    application/xhtml+xml                            xhtml;\n    application/xspf+xml                             xspf;\n    application/zip                                  zip;\n\n    application/octet-stream                         bin exe dll;\n    application/octet-stream                         deb;\n    application/octet-stream                         dmg;\n    application/octet-stream                         iso img;\n    application/octet-stream                         msi msp msm;\n\n    audio/midi                                       mid midi kar;\n    audio/mpeg                                       mp3;\n    audio/ogg                                        ogg;\n    audio/x-m4a                                      m4a;\n    audio/x-realaudio                                ra;\n\n    video/3gpp                                       3gpp 3gp;\n    video/mp2t                                       ts;\n    video/mp4                                        mp4;\n    video/mpeg                                       mpeg mpg;\n    video/quicktime                                  mov;\n    video/webm                                       webm;\n    video/x-flv                                      flv;\n    video/x-m4v                                      m4v;\n    video/x-mng                                      mng;\n    video/x-ms-asf                                   asx asf;\n    video/x-ms-wmv                                   wmv;\n    video/x-msvideo                                  avi;\n}\n"
  },
  {
    "path": "docker/rootfs/etc/nginx/nginx.conf",
    "content": "# run nginx in foreground\ndaemon off;\npid /run/nginx/nginx.pid;\nuser npm;\n\n# Set number of worker processes automatically based on number of CPU cores.\nworker_processes auto;\n\n# Enables the use of JIT for regular expressions to speed-up their processing.\npcre_jit on;\n\nerror_log /data/logs/fallback_error.log warn;\n\n# Includes files with directives to load dynamic modules.\ninclude /etc/nginx/modules/*.conf;\n\n# Custom\ninclude /data/nginx/custom/root_top[.]conf;\n\nevents {\n\tinclude /data/nginx/custom/events[.]conf;\n}\n\nhttp {\n\tinclude                       /etc/nginx/mime.types;\n\tdefault_type                  application/octet-stream;\n\tsendfile                      on;\n\tserver_tokens                 off;\n\ttcp_nopush                    on;\n\ttcp_nodelay                   on;\n\tclient_body_temp_path         /tmp/nginx/body 1 2;\n\tkeepalive_timeout             90s;\n\tproxy_connect_timeout         90s;\n\tproxy_send_timeout            90s;\n\tproxy_read_timeout            90s;\n\tssl_prefer_server_ciphers     on;\n\tgzip                          on;\n\tproxy_ignore_client_abort     off;\n\tclient_max_body_size          2000m;\n\tserver_names_hash_bucket_size 1024;\n\tproxy_http_version            1.1;\n\tproxy_set_header              X-Forwarded-Scheme $scheme;\n\tproxy_set_header              X-Forwarded-For $proxy_add_x_forwarded_for;\n\tproxy_set_header              Accept-Encoding \"\";\n\tproxy_cache                   off;\n\tproxy_cache_path              /var/lib/nginx/cache/public  levels=1:2 keys_zone=public-cache:30m max_size=192m;\n\tproxy_cache_path              /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;\n\n\t# Log format and fallback log file\n\tinclude /etc/nginx/conf.d/include/log-proxy[.]conf;\n\n\t# Dynamically generated resolvers file\n\tinclude /etc/nginx/conf.d/include/resolvers[.]conf;\n\n\t# Default upstream scheme\n\tmap $host $forward_scheme {\n\t\tdefault http;\n\t}\n\n\t# Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header\n\tmap $http_x_forwarded_proto $x_forwarded_proto {\n\t\t\"http\" \"http\";\n\t\t\"https\" \"https\";\n\t\tdefault $scheme;\n\t}\n\tmap $http_x_forwarded_scheme $x_forwarded_scheme {\n\t\t\"http\" \"http\";\n\t\t\"https\" \"https\";\n\t\tdefault $scheme;\n\t}\n\n\t# Real IP Determination\n\n\t# Local subnets:\n\tset_real_ip_from 10.0.0.0/8;\n\tset_real_ip_from 172.16.0.0/12; # Includes Docker subnet\n\tset_real_ip_from 192.168.0.0/16;\n\t# NPM generated CDN ip ranges:\n\tinclude conf.d/include/ip_ranges[.]conf;\n\t# always put the following 2 lines after ip subnets:\n\treal_ip_header X-Real-IP;\n\treal_ip_recursive on;\n\n\t# Custom\n\tinclude /data/nginx/custom/http_top[.]conf;\n\n\t# Files generated by NPM\n\tinclude /etc/nginx/conf.d/*.conf;\n\tinclude /data/nginx/default_host/*.conf;\n\tinclude /data/nginx/proxy_host/*.conf;\n\tinclude /data/nginx/redirection_host/*.conf;\n\tinclude /data/nginx/dead_host/*.conf;\n\tinclude /data/nginx/temp/*.conf;\n\n\t# Custom\n\tinclude /data/nginx/custom/http[.]conf;\n}\n\nstream {\n\t# Log format and fallback log file\n\tinclude /etc/nginx/conf.d/include/log-stream[.]conf;\n\n\t# Files generated by NPM\n\tinclude /data/nginx/stream/*.conf;\n\n\t# Custom\n\tinclude /data/nginx/custom/stream[.]conf;\n}\n\n# Custom\ninclude /data/nginx/custom/root[.]conf;\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\n. /usr/bin/common.sh\n\ncd /app || exit 1\n\nlog_info 'Starting backend ...'\n\nif [ \"${DEVELOPMENT:-}\" = 'true' ]; then\n\ts6-setuidgid \"$PUID:$PGID\" yarn install\n\texec s6-setuidgid \"$PUID:$PGID\" bash -c \"export HOME=$NPMHOME;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js\"\nelse\n\twhile :\n\tdo\n\t\ts6-setuidgid \"$PUID:$PGID\" bash -c \"export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js\"\n\t\tsleep 1\n\tdone\nfi\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type",
    "content": "longrun\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\n# This service is DEVELOPMENT only.\n\nif [ \"$DEVELOPMENT\" = 'true' ]; then\n\t. /usr/bin/common.sh\n\tcd /frontend || exit 1\n\tHOME=$NPMHOME\n\texport HOME\n\tmkdir -p /frontend/dist\n\tchown -R \"$PUID:$PGID\" /frontend/dist\n\n\tlog_info 'Starting frontend ...'\n\ts6-setuidgid \"$PUID:$PGID\" yarn install\n\texec s6-setuidgid \"$PUID:$PGID\" yarn dev\nelse\n\texit 0\nfi\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type",
    "content": "longrun\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\n. /usr/bin/common.sh\n\nlog_info 'Starting nginx ...'\nexec s6-setuidgid \"$PUID:$PGID\" nginx\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type",
    "content": "longrun\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\n. /usr/bin/common.sh\n\nif [ \"$(id -u)\" != \"0\" ]; then\n\tlog_fatal \"This docker container must be run as root, do not specify a user.\\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization.\"\nfi\n\nif [ \"$DEBUG\" = \"true\" ]; then\n\tset -x\nfi\n\n. /etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh\n. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh\n. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh\n. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh\n. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh\n. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh\n. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\nlog_info \"Configuring $NPMUSER user ...\"\n\nif id -u \"$NPMUSER\" 2>/dev/null; then\n\t# user already exists\n\tusermod -u \"$PUID\" \"$NPMUSER\"\nelse\n\t# Add user\n\tuseradd -o -u \"$PUID\" -U -d \"$NPMHOME\" -s /bin/false \"$NPMUSER\"\nfi\n\nlog_info \"Configuring $NPMGROUP group ...\"\nif [ \"$(get_group_id \"$NPMGROUP\")\" = '' ]; then\n\t# Add group. This will not set the id properly if it's already taken\n\tgroupadd -f -g \"$PGID\" \"$NPMGROUP\"\nelse\n\tgroupmod -o -g \"$PGID\" \"$NPMGROUP\"\nfi\n\n# Set the group ID and check it\ngroupmod -o -g \"$PGID\" \"$NPMGROUP\"\nif [ \"$(get_group_id \"$NPMGROUP\")\" != \"$PGID\" ]; then\n\techo \"ERROR: Unable to set group id properly\"\n\texit 1\nfi\n\n# Set the group against the user and check it\nusermod -G \"$PGID\" \"$NPMGROUP\"\nif [ \"$(id -g \"$NPMUSER\")\" != \"$PGID\" ] ; then\n\techo \"ERROR: Unable to set group against the user properly\"\n\texit 1\nfi\n\n# Home for user\nmkdir -p \"$NPMHOME\"\nchown -R \"$PUID:$PGID\" \"$NPMHOME\"\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\nlog_info 'Checking paths ...'\n\n# Ensure /data is mounted\nif [ ! -d '/data' ]; then\n\tlog_fatal '/data is not mounted! Check your docker configuration.'\nfi\n# Ensure /etc/letsencrypt is mounted\nif [ ! -d '/etc/letsencrypt' ]; then\n\tlog_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.'\nfi\n\n# Create required folders\nmkdir -p \\\n\t/data/nginx \\\n\t/data/custom_ssl \\\n\t/data/logs \\\n\t/data/access \\\n\t/data/nginx/default_host \\\n\t/data/nginx/default_www \\\n\t/data/nginx/proxy_host \\\n\t/data/nginx/redirection_host \\\n\t/data/nginx/stream \\\n\t/data/nginx/dead_host \\\n\t/data/nginx/temp \\\n\t/data/letsencrypt-acme-challenge \\\n\t/run/nginx \\\n\t/tmp/nginx/body \\\n\t/var/log/nginx \\\n\t/var/lib/nginx/cache/public \\\n\t/var/lib/nginx/cache/private \\\n\t/var/cache/nginx/proxy_temp\n\ntouch /var/log/nginx/error.log || true\nchmod 777 /var/log/nginx/error.log || true\nchmod -R 777 /var/cache/nginx || true\nchmod 644 /etc/logrotate.d/nginx-proxy-manager\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\nlog_info 'Setting ownership ...'\n\n# root\nchown root /tmp/nginx\n\nlocations=(\n\t\"/data\"\n\t\"/etc/letsencrypt\"\n\t\"/run/nginx\"\n\t\"/tmp/nginx\"\n\t\"/var/cache/nginx\"\n\t\"/var/lib/logrotate\"\n\t\"/var/lib/nginx\"\n\t\"/var/log/nginx\"\n\t\"/etc/nginx/nginx\"\n\t\"/etc/nginx/nginx.conf\"\n\t\"/etc/nginx/conf.d\"\n)\n\nchownit() {\n\tlocal dir=\"$1\"\n\tlocal recursive=\"${2:-true}\"\n\n\tlocal have\n\thave=\"$(stat -c '%u:%g' \"$dir\")\"\n\techo \"- $dir ... \"\n\n\tif [ \"$have\" != \"$PUID:$PGID\" ]; then\n\t\tif [ \"$recursive\" = 'true' ] && [ -d \"$dir\" ]; then\n\t\t\tchown -R \"$PUID:$PGID\" \"$dir\"\n\t\telse\n\t\t\tchown \"$PUID:$PGID\" \"$dir\"\n\t\tfi\n\t\techo \"    DONE\"\n\telse\n\t\techo \"    SKIPPED\"\n\tfi\n}\n\nfor loc in \"${locations[@]}\"; do\n\tchownit \"$loc\"\ndone\n\nif [ \"$(is_true \"${SKIP_CERTBOT_OWNERSHIP:-}\")\" = '1' ]; then\n\tlog_info 'Skipping ownership change of certbot directories'\nelse\n\tlog_info 'Changing ownership of certbot directories, this may take some time ...'\n\tchownit \"/opt/certbot\" false\n\tchownit \"/opt/certbot/bin\" false\n\n\t# Handle all site-packages directories efficiently\n\tfind /opt/certbot/lib -type d -name \"site-packages\" | while read -r SITE_PACKAGES_DIR; do\n\t\tchownit \"$SITE_PACKAGES_DIR\"\n\tdone\nfi\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\nlog_info 'Dynamic resolvers ...'\n\n# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`\n# thanks @tfmm\nif [ \"$(is_true \"${DISABLE_RESOLVER:-}\")\" = '0' ]; then\n\tif [ \"$(is_true \"${DISABLE_IPV6:-}\")\" = '1' ]; then\n\t\techo resolver \"$(awk 'BEGIN{ORS=\" \"} $1==\"nameserver\" { sub(/%.*$/,\"\",$2); print ($2 ~ \":\")? \"[\"$2\"]\": $2}' /etc/resolv.conf) ipv6=off valid=10s;\" > /etc/nginx/conf.d/include/resolvers.conf\n\telse\n\t\techo resolver \"$(awk 'BEGIN{ORS=\" \"} $1==\"nameserver\" { sub(/%.*$/,\"\",$2); print ($2 ~ \":\")? \"[\"$2\"]\": $2}' /etc/resolv.conf) valid=10s;\" > /etc/nginx/conf.d/include/resolvers.conf\n\tfi\nfi\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\n# This command reads the `DISABLE_IPV6` env var and will either enable\n# or disable ipv6 in all nginx configs based on this setting.\n\nset -e\n\nlog_info 'IPv6 ...'\n\nprocess_folder () {\n\tFILES=$(find \"$1\" -type f -name \"*.conf\")\n\tSED_REGEX=\n\n\tif [ \"$(is_true \"${DISABLE_IPV6:-}\")\" = '1' ]; then\n\t\t# IPV6 is disabled\n\t\techo \"Disabling IPV6 in hosts in: $1\"\n\t\tSED_REGEX='s/^([^#]*)listen \\[::\\]/\\1#listen [::]/g'\n\telse\n\t\t# IPV6 is enabled\n\t\techo \"Enabling IPV6 in hosts in: $1\"\n\t\tSED_REGEX='s/^(\\s*)#listen \\[::\\]/\\1listen [::]/g'\n\tfi\n\n\tfor FILE in $FILES\n\tdo\n\t\techo \"- ${FILE}\"\n\t\tTMPFILE=\"${FILE}.tmp\"\n\t\tif sed -E \"$SED_REGEX\" \"$FILE\" > \"$TMPFILE\" && [ -s \"$TMPFILE\" ]; then\n\t\t\tmv \"$TMPFILE\" \"$FILE\"\n\t\telse\n\t\t\techo \"WARNING: skipping ${FILE} — sed produced empty output\" >&2\n\t\t\trm -f \"$TMPFILE\"\n\t\tfi\n\tdone\n\n\t# ensure the files are still owned by the npm user\n\tchown -R \"$PUID:$PGID\" \"$1\"\n}\n\nprocess_folder /etc/nginx/conf.d\nprocess_folder /data/nginx\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\n\n# in s6, environmental variables are written as text files for s6 to monitor\n# search through full-path filenames for files ending in \"__FILE\"\nlog_info 'Docker secrets ...'\n\nfor FILENAME in $(find /var/run/s6/container_environment/ | grep \"__FILE$\"); do\n\techo \"[secret-init] Evaluating ${FILENAME##*/} ...\"\n\n\t# set SECRETFILE to the contents of the full-path textfile\n\tSECRETFILE=$(cat \"${FILENAME}\")\n\t# if SECRETFILE exists / is not null\n\tif [[ -f \"${SECRETFILE}\" ]]; then\n\t\t# strip the appended \"__FILE\" from environmental variable name ...\n\t\tSTRIPFILE=$(echo \"${FILENAME}\" | sed \"s/__FILE//g\")\n\t\t# echo \"[secret-init] Set STRIPFILE to ${STRIPFILE}\"  # DEBUG - rm for prod!\n\n\t\t# ... and set value to contents of secretfile\n\t\t# since s6 uses text files, this is effectively \"export ...\"\n\t\tprintf $(cat \"${SECRETFILE}\") > \"${STRIPFILE}\"\n\t\t# echo \"[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})\"  # DEBUG - rm for prod!\"\n\t\techo \"Success: ${STRIPFILE##*/} set from ${FILENAME##*/}\"\n\n\telse\n\t\techo \"Cannot find secret in ${FILENAME}\"\n\tfi\ndone\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh",
    "content": "#!/command/with-contenv bash\n# shellcheck shell=bash\n\nset -e\nset +x\n\necho \"\n-------------------------------------\n _   _ ____  __  __\n| \\ | |  _ \\|  \\/  |\n|  \\| | |_) | |\\/| |\n| |\\  |  __/| |  | |\n|_| \\_|_|   |_|  |_|\n-------------------------------------\nUser:  $NPMUSER PUID:$PUID ID:$(id -u \"$NPMUSER\") GROUP:$(id -g \"$NPMUSER\")\nGroup: $NPMGROUP PGID:$PGID ID:$(get_group_id \"$NPMGROUP\")\n-------------------------------------\n\"\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type",
    "content": "oneshot\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up",
    "content": "# shellcheck shell=bash\n/etc/s6-overlay/s6-rc.d/prepare/00-all.sh\n"
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx",
    "content": ""
  },
  {
    "path": "docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare",
    "content": ""
  },
  {
    "path": "docker/rootfs/root/.bashrc",
    "content": "#!/bin/bash\n\nif [ -t 1 ]; then\n\texport PS1=\"\\e[1;34m[\\e[1;33m\\u@\\e[1;32mdocker-\\h\\e[1;37m:\\w\\[\\e[1;34m]\\e[1;36m\\\\$ \\e[0m\"\nfi\n\n# Aliases\nalias l='ls -lAsh --color'\nalias ls='ls -C1 --color'\nalias cp='cp -ip'\nalias rm='rm -i'\nalias mv='mv -i'\nalias h='cd ~;clear;'\n\n. /etc/os-release\n\necho -e -n '\\E[1;34m'\nfiglet -w 120 \"NginxProxyManager\"\necho -e \"\\E[1;36mVersion \\E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\\E[1;36m, OpenResty \\E[1;32m${OPENRESTY_VERSION:-unknown}\\E[1;36m, ${ID:-debian} \\E[1;32m${VERSION:-unknown}\\E[1;36m, Certbot \\E[1;32m$(certbot --version)\\E[0m\"\necho -e -n '\\E[1;34m'\ncat /built-for-arch\necho -e '\\E[0m'\n"
  },
  {
    "path": "docker/rootfs/usr/bin/check-health",
    "content": "#!/bin/bash\n\nOK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status')\n\nif [ \"$OK\" == \"OK\" ]; then\n\techo \"OK\"\n\texit 0\nelse\n\techo \"NOT OK\"\n\texit 1\nfi\n"
  },
  {
    "path": "docker/rootfs/usr/bin/common.sh",
    "content": "#!/bin/bash\n\nset -e\n\nCYAN='\\E[1;36m'\nBLUE='\\E[1;34m'\nYELLOW='\\E[1;33m'\nRED='\\E[1;31m'\nRESET='\\E[0m'\nexport CYAN BLUE YELLOW RED RESET\n\nPUID=${PUID:-0}\nPGID=${PGID:-0}\n\n# If changing the username and group name below,\n# ensure all references to this user is also changed.\n# See docker/rootfs/etc/logrotate.d/nginx-proxy-manager\n# and docker/rootfs/etc/nginx/nginx.conf\nNPMUSER=npm\nNPMGROUP=npm\nNPMHOME=/tmp/npmuserhome\nexport NPMUSER NPMGROUP NPMHOME\n\nif [[ \"$PUID\" -ne '0' ]] && [ \"$PGID\" = '0' ]; then\n\t# set group id to same as user id,\n\t# the user probably forgot to specify the group id and\n\t# it would be rediculous to intentionally use the root group\n\t# for a non-root user\n\tPGID=$PUID\nfi\n\nexport PUID PGID\n\nlog_info () {\n\techo -e \"${BLUE}❯ ${CYAN}$1${RESET}\"\n}\n\nlog_error () {\n\techo -e \"${RED}❯ $1${RESET}\"\n}\n\n# The `run` file will only execute 1 line so this helps keep things\n# logically separated\n\nlog_fatal () {\n\techo -e \"${RED}--------------------------------------${RESET}\"\n\techo -e \"${RED}ERROR: $1${RESET}\"\n\techo -e \"${RED}--------------------------------------${RESET}\"\n\t/run/s6/basedir/bin/halt\n\texit 1\n}\n\n# param $1: group_name\nget_group_id () {\n\tif [ \"${1:-}\" != '' ]; then\n\t\tgetent group \"$1\" | cut -d: -f3\n\tfi\n}\n\n# param $1: value\nis_true () {\n\tVAL=$(echo \"${1:-}\" | tr '[:upper:]' '[:lower:]')\n\tif [ \"$VAL\" == 'true' ] || [ \"$VAL\" == 'on' ] || [ \"$VAL\" == '1' ] || [ \"$VAL\" == 'yes' ]; then\n\t\techo '1'\n\telse\n\t\techo '0'\n\tfi\n}\n"
  },
  {
    "path": "docker/rootfs/var/www/html/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        <title>Default Site</title>\n        <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\" rel=\"stylesheet\">\n        <style>\n            .jumbotron { margin-top: 50px; }\n        </style>\n    </head>\n    <body>\n        <div class=\"container\">\n            <div class=\"jumbotron\">\n                <h1>Congratulations!</h1>\n                <p>You've successfully started the Nginx Proxy Manager.</p>\n                <p>If you're seeing this site then you're trying to access a host that isn't set up yet.</p>\n                <p>Log in to the Admin panel to get started.</p>\n            </div>\n            <p class=\"text-center\"><small>Powered by <a href=\"https://github.com/jc21/nginx-proxy-manager\" target=\"_blank\">Nginx Proxy Manager</a></small></p>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "docker/scripts/install-s6",
    "content": "#!/bin/bash -e\n\n# Note: This script is designed to be run inside a Docker Build for a container\n\nCYAN='\\E[1;36m'\nYELLOW='\\E[1;33m'\nBLUE='\\E[1;34m'\nGREEN='\\E[1;32m'\nRESET='\\E[0m'\n\nS6_OVERLAY_VERSION=3.2.1.0\nTARGETPLATFORM=${1:-linux/amd64}\n\n# Determine the correct binary file for the architecture given\ncase $TARGETPLATFORM in\n\tlinux/arm64)\n\t\tS6_ARCH=aarch64\n\t\t;;\n\n\t*)\n\t\tS6_ARCH=x86_64\n\t\t;;\nesac\n\necho -e \"${BLUE}❯ ${CYAN}Installing S6-overlay v${S6_OVERLAY_VERSION} for ${YELLOW}${TARGETPLATFORM} (${S6_ARCH})${RESET}\"\n\ncurl -L -o '/tmp/s6-overlay-noarch.tar.xz' \"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz\"\ncurl -L -o \"/tmp/s6-overlay-${S6_ARCH}.tar.xz\" \"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz\"\ntar -C / -Jxpf '/tmp/s6-overlay-noarch.tar.xz'\ntar -C / -Jxpf \"/tmp/s6-overlay-${S6_ARCH}.tar.xz\"\n\nrm -rf \"/tmp/s6-overlay-${S6_ARCH}.tar.xz\"\n\necho -e \"${BLUE}❯ ${GREEN}S6-overlay install Complete${RESET}\"\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "dist\nnode_modules\nts\n.temp\n.cache\n.vitepress/cache\n\n.yarn/*\n!.yarn/releases\n!.yarn/plugins\n!.yarn/sdks\n!.yarn/versions\n*.gz\n*.tgz\n"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "content": "import { defineConfig, type DefaultTheme } from 'vitepress';\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n\ttitle: \"Nginx Proxy Manager\",\n\tdescription: \"Expose your services easily and securely\",\n\thead: [\n\t\t[\"link\", { rel: \"icon\", href: \"/icon.png\" }],\n\t\t[\"meta\", { name: \"description\", content: \"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt\" }],\n\t\t[\"meta\", { property: \"og:title\", content: \"Nginx Proxy Manager\" }],\n\t\t[\"meta\", { property: \"og:description\", content: \"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt\"}],\n\t\t[\"meta\", { property: \"og:type\", content: \"website\" }],\n\t\t[\"meta\", { property: \"og:url\", content: \"https://nginxproxymanager.com/\" }],\n\t\t[\"meta\", { property: \"og:image\", content: \"https://nginxproxymanager.com/icon.png\" }],\n\t\t[\"meta\", { name: \"twitter:card\", content: \"summary\"}],\n\t\t[\"meta\", { name: \"twitter:title\", content: \"Nginx Proxy Manager\"}],\n\t\t[\"meta\", { name: \"twitter:description\", content: \"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt\"}],\n\t\t[\"meta\", { name: \"twitter:image\", content: \"https://nginxproxymanager.com/icon.png\"}],\n\t\t[\"meta\", { name: \"twitter:alt\", content: \"Nginx Proxy Manager\"}],\n\t\t// GA\n\t\t['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}],\n\t\t['script', {}, \"window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', 'G-TXT8F5WY5B');\"],\n\t],\n\tsitemap: {\n\t\thostname: 'https://nginxproxymanager.com'\n\t},\n\tmetaChunk: true,\n\tsrcDir: './src',\n\toutDir: './dist',\n\tthemeConfig: {\n\t\t// https://vitepress.dev/reference/default-theme-config\n\t\tlogo: { src: '/logo.svg', width: 24, height: 24 },\n\t\tnav: [\n\t\t\t{ text: 'Setup', link: '/setup/' },\n\t\t],\n\t\tsidebar: [\n\t\t\t{\n\t\t\t\titems: [\n\t\t\t\t\t// { text: 'Home', link: '/' },\n\t\t\t\t\t{ text: 'Guide', link: '/guide/' },\n\t\t\t\t\t{ text: 'Screenshots', link: '/screenshots/' },\n\t\t\t\t\t{ text: 'Setup Instructions', link: '/setup/' },\n\t\t\t\t\t{ text: 'Advanced Configuration', link: '/advanced-config/' },\n\t\t\t\t\t{ text: 'Upgrading', link: '/upgrading/' },\n\t\t\t\t\t{ text: 'Frequently Asked Questions', link: '/faq/' },\n\t\t\t\t\t{ text: 'Third Party', link: '/third-party/' },\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tsocialLinks: [\n\t\t\t{ icon: 'github', link: 'https://github.com/NginxProxyManager/nginx-proxy-manager' }\n\t\t],\n\t\tsearch: {\n\t\t\tprovider: 'local'\n\t\t},\n\t\tfooter: {\n\t\t\tmessage: 'Released under the MIT License.',\n\t\t\tcopyright: 'Copyright © 2016-present jc21.com'\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "docs/.vitepress/theme/custom.css",
    "content": ":root {\n\t--vp-home-hero-name-color: transparent;\n\t--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #f15833 30%, #FAA42F);\n\n\t--vp-home-hero-image-background-image: linear-gradient(-45deg, #aaaaaa 50%, #777777 50%);\n\t--vp-home-hero-image-filter: blur(44px);\n\n\t--vp-c-brand-1: #f15833;\n\t--vp-c-brand-2: #FAA42F;\n\t--vp-c-brand-3: #f15833;\n}\n\n  @media (min-width: 640px) {\n\t:root {\n\t\t--vp-home-hero-image-filter: blur(56px);\n\t}\n}\n\n  @media (min-width: 960px) {\n\t:root {\n\t\t--vp-home-hero-image-filter: blur(68px);\n\t}\n}\n\n.inline-img img {\n\tdisplay: inline;\n\tmargin-right: 8px;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "content": "import DefaultTheme from 'vitepress/theme'\nimport './custom.css'\n\nexport default DefaultTheme\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"scripts\": {\n    \"dev\": \"vitepress dev --host\",\n    \"build\": \"vitepress build\",\n    \"preview\": \"vitepress preview\",\n    \"set-version\": \"./scripts/set-version.sh\"\n  },\n  \"devDependencies\": {\n    \"vitepress\": \"^1.6.4\"\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "docs/scripts/set-version.sh",
    "content": "#!/bin/bash\nset -euf\n\n# this script accepts a version number as an argument\n# and replaces {{VERSION}} in src/*.md with the provided version number.\n\nif [ \"$#\" -ne 1 ]; then\n\techo \"Usage: $0 <version>\"\n\texit 1\nfi\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$DIR/..\" || exit 1\n\nVERSION=\"$1\"\n# find all .md files in src/ and replace {{VERSION}} with the provided version number\nfind src/ -type f -name \"*.md\" -exec sed -i \"s/{{VERSION}}/$VERSION/g\" {} \\;\n"
  },
  {
    "path": "docs/src/advanced-config/index.md",
    "content": "---\noutline: deep\n---\n\n# Advanced Configuration\n\n## Running processes as a user/group\n\nBy default, the services (nginx etc) will run as `root` user inside the docker container.\nYou can change this behaviour by setting the following environment variables.\nNot only will they run the services as this user/group, they will change the ownership\non the `data` and `letsencrypt` folders at startup.\n\n```yml\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    environment:\n      PUID: 1000\n      PGID: 1000\n    # ...\n```\n\nThis may have the side effect of a failed container start due to permission denied trying\nto open port 80 on some systems. The only course to fix that is to remove the variables\nand run as the default root user.\n\n## Best Practice: Use a Docker network\n\nFor those who have a few of their upstream services running in Docker on the same Docker\nhost as NPM, here's a trick to secure things a bit better. By creating a custom Docker network,\nyou don't need to publish ports for your upstream services to all of the Docker host's interfaces.\n\nCreate a network, ie \"scoobydoo\":\n\n```bash\ndocker network create scoobydoo\n```\n\nThen add the following to the `docker-compose.yml` file for both NPM and any other\nservices running on this Docker host:\n\n```yml\nnetworks:\n  default:\n    external: true\n    name: scoobydoo\n```\n\nLet's look at a Portainer example:\n\n```yml\nservices:\n\n  portainer:\n    image: portainer/portainer\n    privileged: true\n    volumes:\n      - './data:/data'\n      - '/var/run/docker.sock:/var/run/docker.sock'\n    restart: unless-stopped\n\nnetworks:\n  default:\n    external: true\n    name: scoobydoo\n```\n\nNow in the NPM UI you can create a proxy host with `portainer` as the hostname,\nand port `9000` as the port. Even though this port isn't listed in the docker-compose\nfile, it's \"exposed\" by the Portainer Docker image for you and not available on\nthe Docker host outside of this Docker network. The service name is used as the\nhostname, so make sure your service names are unique when using the same network.\n\n## Docker Healthcheck\n\nThe `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this\nfeature by adding the following to the service in your `docker-compose.yml` file:\n\n```yml\nhealthcheck:\n  test: [\"CMD\", \"/usr/bin/check-health\"]\n  interval: 10s\n  timeout: 3s\n```\n\n## Docker File Secrets\n\nThis image supports the use of Docker secrets to import from files and keep sensitive usernames or passwords from being passed or preserved in plaintext.\n\nYou can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name.\n\n```yml\nsecrets:\n  # Secrets are single-line text files where the sole content is the secret\n  # Paths in this example assume that secrets are kept in local folder called \".secrets\"\n  DB_ROOT_PWD:\n    file: .secrets/db_root_pwd.txt\n  MYSQL_PWD:\n    file: .secrets/mysql_pwd.txt\n\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    restart: unless-stopped\n    ports:\n      # Public HTTP Port:\n      - '80:80'\n      # Public HTTPS Port:\n      - '443:443'\n      # Admin Web Port:\n      - '81:81'\n    environment:\n      # These are the settings to access your db\n      DB_MYSQL_HOST: \"db\"\n      DB_MYSQL_PORT: 3306\n      DB_MYSQL_USER: \"npm\"\n      # DB_MYSQL_PASSWORD: \"npm\"  # use secret instead\n      DB_MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD\n      DB_MYSQL_NAME: \"npm\"\n      # If you would rather use Sqlite, remove all DB_MYSQL_* lines above\n      # Uncomment this if IPv6 is not enabled on your host\n      # DISABLE_IPV6: 'true'\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n    secrets:\n      - MYSQL_PWD\n    depends_on:\n      - db\n\n  db:\n    image: 'linuxserver/mariadb'\n    restart: unless-stopped\n    environment:\n      MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD\n      MYSQL_DATABASE: 'npm'\n      MYSQL_USER: 'npm'\n      MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD\n      TZ: 'Australia/Brisbane'\n    volumes:\n      - ./mariadb:/config\n    secrets:\n      - DB_ROOT_PWD\n      - MYSQL_PWD\n```\n\n\n## Disabling IPv6\n\nOn some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log:\n\n> Address family not supported by protocol\n\nThe easy fix is to add a Docker environment variable to the Nginx Proxy Manager stack:\n\n```yml\n    environment:\n      DISABLE_IPV6: 'true'\n```\n\n## Disabling IP Ranges Fetch\n\nBy default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled:\n\n```yml\n    environment:\n      IP_RANGES_FETCH_ENABLED: 'false'\n```\n\n## Custom Nginx Configurations\n\nIf you are a more advanced user, you might be itching for extra Nginx customizability.\n\nNPM has the ability to include different custom configuration snippets in different places.\n\nYou can add your custom configuration snippet files at `/data/nginx/custom` as follow:\n\n - `/data/nginx/custom/root_top.conf`: Included at the top of nginx.conf\n - `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf\n - `/data/nginx/custom/http_top.conf`: Included at the top of the main http block\n - `/data/nginx/custom/http.conf`: Included at the end of the main http block\n - `/data/nginx/custom/events.conf`: Included at the end of the events block\n - `/data/nginx/custom/stream.conf`: Included at the end of the main stream block\n - `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block\n - `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block\n - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block\n - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block\n - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block\n - `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block\n\nEvery file is optional.\n\n\n## X-FRAME-OPTIONS Header\n\nYou can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header\nvalue by specifying it as a Docker environment variable. The default if not specified is `deny`.\n\n```yml\n  ...\n  environment:\n    X_FRAME_OPTIONS: \"sameorigin\"\n  ...\n```\n\n## Customising logrotate settings\n\nBy default, NPM rotates the access- and error logs weekly and keeps 4 and 10 log files respectively.\nDepending on the usage, this can lead to large log files, especially access logs.\nYou can customise the logrotate configuration through a mount (if your custom config is `logrotate.custom`):\n\n```yml\n  volumes:\n    ...\n    - ./logrotate.custom:/etc/logrotate.d/nginx-proxy-manager\n```\n\nFor reference, the default configuration can be found [here](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/rootfs/etc/logrotate.d/nginx-proxy-manager).\n\n## Enabling the geoip2 module\n\nTo enable the geoip2 module, you can create the custom configuration file `/data/nginx/custom/root_top.conf` and include the following snippet:\n\n```\nload_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so;\nload_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;\n```\n\n## Auto Initial User Creation\n\nSetting these environment variables will create the default user on startup, skipping the UI first user setup screen:\n\n```yml\n    environment:\n      INITIAL_ADMIN_EMAIL: my@example.com\n      INITIAL_ADMIN_PASSWORD: mypassword1\n```\n\n## Disable Nginx Resolver\n\nOn startup, we generate a resolvers directive for Nginx unless this is defined:\n\n```yml\n    environment:\n      DISABLE_RESOLVER: true\n```\n\nIn this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file\nand then the `/etc/resolv.conf`.\n"
  },
  {
    "path": "docs/src/faq/index.md",
    "content": "---\noutline: deep\n---\n\n# FAQ\n\n## Do I have to use Docker?\n\nYes, that's how this project is packaged.\n\nThis makes it easier to support the project when we have control over the version of Nginx other packages\nuse by the project.\n\n## Can I run it on a Raspberry Pi?\n\nYes! The docker image is multi-arch and is built for a variety of architectures. If yours is\n[not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a\n[GitHub issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).\n\n## I can't get my service to proxy properly?\n\nYour best bet is to ask the [Reddit community for support](https://www.reddit.com/r/nginxproxymanager/). There's safety in numbers.\n\n## When adding username and password access control to a proxy host, I can no longer login into the app.\n\nHaving an Access Control List (ACL) with username and password requires the browser to always send this username\nand password in the `Authorization` header on each request. If your proxied app also requires authentication (like\nNginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information,\nas this is the standardized header meant for this kind of information. However having multiples of the same headers\nis not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps\ndo not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can\nonly be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization.\n"
  },
  {
    "path": "docs/src/guide/index.md",
    "content": "---\noutline: deep\n---\n\n# Guide\n\n::: raw\n<p align=\"center\">\n\t<a href=\"https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager\" style=\"display:inline;margin-right:5px;\">\n\t\t<img src=\"https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge\" style=\"display:inline;\">\n\t</a>\n\t<a href=\"https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager\" style=\"display:inline;margin-right:5px;\">\n\t\t<img src=\"https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge\" style=\"display:inline;\">\n\t</a>\n</p>\n:::\n\nThis project comes as a pre-built docker image that enables you to easily forward to your websites\nrunning at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.\n\n- [Quick Setup](#quick-setup)\n- [Full Setup](/setup/)\n- [Screenshots](/screenshots/)\n\n## Project Goal\n\nI created this project to fill a personal need to provide users with an easy way to accomplish reverse\nproxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.\nWhile there might be advanced options they are optional and the project should be as simple as possible\nso that the barrier for entry here is low.\n\n::: raw\n<a href=\"https://www.buymeacoffee.com/jc21\" target=\"_blank\"><img src=\"http://public.jc21.com/github/by-me-a-coffee.png\" alt=\"Buy Me A Coffee\" style=\"height: 51px !important;width: 217px !important;\" ></a>\n:::\n\n## Features\n\n- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.io/)\n- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx\n- Free SSL using Let's Encrypt or provide your own custom SSL certificates\n- Access Lists and basic HTTP Authentication for your hosts\n- Advanced Nginx configuration available for super users\n- User management, permissions and audit log\n\n\n## Hosting your home network\n\nI won't go in to too much detail here but here are the basics for someone new to this self-hosted world.\n\n1. Your home router will have a Port Forwarding section somewhere. Log in and find it\n2. Add port forwarding for port 80 and 443 to the server hosting this project\n3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)\n4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services\n\n## Quick Setup\n\n1. Install Docker and Docker-Compose\n\n- [Docker Install documentation](https://docs.docker.com/get-docker/)\n- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)\n\n2. Create a docker-compose.yml file similar to this:\n\n```yml\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    restart: unless-stopped\n    environment:\n      TZ: \"Australia/Brisbane\"\n    ports:\n      - '80:80'\n      - '81:81'\n      - '443:443'\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n```\n\nThis is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.\n\n3. Bring up your stack by running\n\n```bash\ndocker compose up -d\n```\n\n4. Log in to the Admin UI\n\nWhen your docker container is running, connect to it on port `81` for the admin interface.\n\n[http://127.0.0.1:81](http://127.0.0.1:81)\n\nThis startup can take a minute depending on your hardware.\n\n\n## Contributing\n\nAll are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch.\n\nCI is used in this project. All PR's must pass before being considered. After passing,\ndocker builds for PR's are available on dockerhub for manual verifications.\n\nDocumentation within the `develop` branch is available for preview at\n[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com)\n\n\n### Contributors\n\nSpecial thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).\n\n\n## Getting Support\n\n1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)\n2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)\n3. [Reddit](https://reddit.com/r/nginxproxymanager)\n"
  },
  {
    "path": "docs/src/index.md",
    "content": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\nhero:\n  name: \"Nginx Proxy Manager\"\n  tagline: Expose your services easily and securely\n  image:\n    src: /logo.svg\n    alt: NPM Logo\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /guide/\n    - theme: alt\n      text: GitHub\n      link: https://github.com/NginxProxyManager/nginx-proxy-manager\n\nfeatures:\n  - title: Get Connected\n    details: Expose web services on your network &middot; Free SSL with Let's Encrypt  &middot; Designed with security in mind  &middot; Perfect for home networks\n  - title: Proxy Hosts\n    details: Expose your private network Web services and get connected anywhere.\n  - title: Beautiful UI\n    details: Based on Tabler, the interface is a pleasure to use. Configuring a server has never been so fun.\n  - title: Free SSL\n    details: Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves!\n  - title: Docker FTW\n    details: Built as a Docker Image, Nginx Proxy Manager only requires a database.\n  - title: Multiple Users\n    details: Configure other users to either view or manage their own hosts. Full access permissions are available.\n---\n"
  },
  {
    "path": "docs/src/public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "docs/src/screenshots/index.md",
    "content": "---\noutline: deep\n---\n\n# Screenshots\n\n### Light Mode\n\n::: raw\n<div class=\"inline-img\">\n\t<a href=\"/screenshots/light/01_first-user.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/01_first-user.png\" alt=\"Setup\" title=\"Setup\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/02_login.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/02_login.png\" alt=\"Login\" title=\"Login\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/03_dashboard.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/03_dashboard.png\" alt=\"Dashboard\" title=\"Dashboard\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/04_proxy-hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/04_proxy-hosts.png\" alt=\"Proxy Hosts\" title=\"Proxy Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/05_redirection_hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/05_redirection_hosts.png\" alt=\"Redirection Hosts\" title=\"Redirection Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/06_streams.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/06_streams.png\" alt=\"Streams\" title=\"Streams\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/07_404_hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/07_404_hosts.png\" alt=\"404 Hosts\" title=\"404 Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/08_access-lists.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/08_access-lists.png\" alt=\"Access Lists\" title=\"Access Lists\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/09_certificates.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/09_certificates.png\" alt=\"Certificates\" title=\"Certificates\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/10_users.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/10_users.png\" alt=\"Users\" title=\"Users\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/11_audit-logs.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/11_audit-logs.png\" alt=\"Audit Logs\" title=\"Audit Logs\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/12_settings.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/12_settings.png\" alt=\"Settings\" title=\"Settings\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/13_add-proxy_host.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/13_add-proxy_host.png\" alt=\"Add Proxy Host\" title=\"Add Proxy Host\" width=\"200\"/></a>\n\t<a href=\"/screenshots/light/14_add_proxy_host_dns.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/light/14_add_proxy_host_dns.png\" alt=\"Add Proxy Host with DNS\" title=\"Add Proxy Host with DNS\" width=\"200\"/></a>\n</div>\n:::\n\n### Dark Mode\n\n::: raw\n<div class=\"inline-img\">\n\t<a href=\"/screenshots/dark/01_first-user.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/01_first-user.png\" alt=\"Setup\" title=\"Setup\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/02_login.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/02_login.png\" alt=\"Login\" title=\"Login\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/03_dashboard.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/03_dashboard.png\" alt=\"Dashboard\" title=\"Dashboard\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/04_proxy-hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/04_proxy-hosts.png\" alt=\"Proxy Hosts\" title=\"Proxy Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/05_redirection_hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/05_redirection_hosts.png\" alt=\"Redirection Hosts\" title=\"Redirection Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/06_streams.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/06_streams.png\" alt=\"Streams\" title=\"Streams\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/07_404_hosts.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/07_404_hosts.png\" alt=\"404 Hosts\" title=\"404 Hosts\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/08_access-lists.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/08_access-lists.png\" alt=\"Access Lists\" title=\"Access Lists\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/09_certificates.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/09_certificates.png\" alt=\"Certificates\" title=\"Certificates\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/10_users.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/10_users.png\" alt=\"Users\" title=\"Users\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/11_audit-logs.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/11_audit-logs.png\" alt=\"Audit Logs\" title=\"Audit Logs\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/12_settings.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/12_settings.png\" alt=\"Settings\" title=\"Settings\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/13_add-proxy_host.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/13_add-proxy_host.png\" alt=\"Add Proxy Host\" title=\"Add Proxy Host\" width=\"200\"/></a>\n\t<a href=\"/screenshots/dark/14_add_proxy_host_dns.png\" target=\"_blank\"><img class=\"no-medium-zoom zooming\" src=\"/screenshots/dark/14_add_proxy_host_dns.png\" alt=\"Add Proxy Host with DNS\" title=\"Add Proxy Host with DNS\" width=\"200\"/></a>\n</div>\n:::\n"
  },
  {
    "path": "docs/src/setup/index.md",
    "content": "---\noutline: deep\n---\n\n# Full Setup Instructions\n\n## Running the App\n\nCreate a `docker-compose.yml` file:\n\n```yml\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    restart: unless-stopped\n\n    ports:\n      # These ports are in format <host-port>:<container-port>\n      - '80:80' # Public HTTP Port\n      - '443:443' # Public HTTPS Port\n      - '81:81' # Admin Web Port\n      # Add any other Stream port you want to expose\n      # - '21:21' # FTP\n\n    environment:\n      TZ: \"Australia/Brisbane\"\n\n      # Uncomment this if you want to change the location of\n      # the SQLite DB file within the container\n      # DB_SQLITE_FILE: \"/data/database.sqlite\"\n\n      # Uncomment this if IPv6 is not enabled on your host\n      # DISABLE_IPV6: 'true'\n\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n```\n\nThen:\n\n```bash\ndocker compose up -d\n```\n\n## Using MySQL / MariaDB Database\n\nIf you opt for the MySQL configuration you will have to provide the database server yourself.\n\nIt's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples\nare going to use.\n\nHere is an example of what your `docker-compose.yml` will look like when using a MariaDB container:\n\n```yml\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    restart: unless-stopped\n    ports:\n      # These ports are in format <host-port>:<container-port>\n      - '80:80' # Public HTTP Port\n      - '443:443' # Public HTTPS Port\n      - '81:81' # Admin Web Port\n      # Add any other Stream port you want to expose\n      # - '21:21' # FTP\n    environment:\n      TZ: \"Australia/Brisbane\"\n      # Mysql/Maria connection parameters:\n      DB_MYSQL_HOST: \"db\"\n      DB_MYSQL_PORT: 3306\n      DB_MYSQL_USER: \"npm\"\n      DB_MYSQL_PASSWORD: \"npm\"\n      DB_MYSQL_NAME: \"npm\"\n      # Optional SSL (see section below)\n      # DB_MYSQL_SSL: 'true'\n      # DB_MYSQL_SSL_REJECT_UNAUTHORIZED: 'true'\n      # DB_MYSQL_SSL_VERIFY_IDENTITY: 'true'\n      # Uncomment this if IPv6 is not enabled on your host\n      # DISABLE_IPV6: 'true'\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n    depends_on:\n      - db\n\n  db:\n    image: 'linuxserver/mariadb'\n    restart: unless-stopped\n    environment:\n      MYSQL_ROOT_PASSWORD: 'npm'\n      MYSQL_DATABASE: 'npm'\n      MYSQL_USER: 'npm'\n      MYSQL_PASSWORD: 'npm'\n      TZ: 'Australia/Brisbane'\n    volumes:\n      - ./mariadb:/config\n```\n\n::: warning\nPlease note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use SQLite.\n:::\n\n### Optional: MySQL / MariaDB SSL\n\nYou can enable TLS for the MySQL/MariaDB connection with these environment variables:\n\n- `DB_MYSQL_SSL`: Enable SSL when set to true. If unset or false, SSL disabled (previous default behaviour).\n- `DB_MYSQL_SSL_REJECT_UNAUTHORIZED`: (default: true) Validate the server certificate chain. Set to false to allow self‑signed/unknown CA.\n- `DB_MYSQL_SSL_VERIFY_IDENTITY`: (default: true) Performs host name / identity verification.\n\nEnabling SSL using a self-signed cert (not recommended for production).\n\n## Using Postgres database\n\nSimilar to the MySQL server setup:\n\n```yml\nservices:\n  app:\n    image: 'jc21/nginx-proxy-manager:{{VERSION}}'\n    restart: unless-stopped\n    ports:\n      # These ports are in format <host-port>:<container-port>\n      - '80:80' # Public HTTP Port\n      - '443:443' # Public HTTPS Port\n      - '81:81' # Admin Web Port\n      # Add any other Stream port you want to expose\n      # - '21:21' # FTP\n    environment:\n      TZ: \"Australia/Brisbane\"\n      # Postgres parameters:\n      DB_POSTGRES_HOST: 'db'\n      DB_POSTGRES_PORT: '5432'\n      DB_POSTGRES_USER: 'npm'\n      DB_POSTGRES_PASSWORD: 'npmpass'\n      DB_POSTGRES_NAME: 'npm'\n      # Uncomment this if IPv6 is not enabled on your host\n      # DISABLE_IPV6: 'true'\n    volumes:\n      - ./data:/data\n      - ./letsencrypt:/etc/letsencrypt\n    depends_on:\n      - db\n\n  db:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: 'npm'\n      POSTGRES_PASSWORD: 'npmpass'\n      POSTGRES_DB: 'npm'\n    volumes:\n      - ./postgresql:/var/lib/postgresql\n```\n\n::: warning\n\nCustom Postgres schema is not supported, as such `public` will be used.\n\n:::\n\n## Running on Raspberry PI / ARM devices\n\nThe docker images support the following architectures:\n- amd64\n- arm64\n\n::: warning\n`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please\nuse the `2.13.7` image tag if this applies to you.\n:::\n\nThe docker images are a manifest of all the architecture docker builds supported, so this means\nyou don't have to worry about doing anything special and you can follow the common instructions above.\n\nCheck out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags)\nfor a list of supported architectures and if you want one that doesn't exist,\n[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).\n\nAlso, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/)\non Raspbian.\n\n## Initial Run\n\nAfter the app is running for the first time, the following will happen:\n\n1. JWT keys will be generated and saved in the data folder\n2. The database will initialize with table structures\n3. A default admin user will be created\n\nThis process can take a couple of minutes depending on your machine.\n"
  },
  {
    "path": "docs/src/third-party/index.md",
    "content": "---\noutline: deep\n---\n\n# Third Party\n\nAs this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documentation of those\nintegrations, they are *not supported* by me.\n\nKnown integrations:\n\n- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager)\n- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager)\n- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager)\n- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager)\n- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf)\n\n\nIf you would like your integration of NPM listed, please open a\n[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)\n"
  },
  {
    "path": "docs/src/upgrading/index.md",
    "content": "---\noutline: deep\n---\n\n# Upgrading\n\n```bash\ndocker compose pull\ndocker compose up -d\n```\n\nThis project will automatically update any databases or other requirements so you don't have to follow\nany crazy instructions. These steps above will pull the latest updates and recreate the docker\ncontainers.\n\nSee the [list of releases](https://github.com/NginxProxyManager/nginx-proxy-manager/releases) for any upgrade steps specific to each release.\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "src/locale/lang\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "frontend/biome.json",
    "content": "{\n    \"$schema\": \"https://biomejs.dev/schemas/2.4.5/schema.json\",\n    \"vcs\": {\n        \"enabled\": true,\n        \"clientKind\": \"git\",\n        \"useIgnoreFile\": true\n    },\n    \"files\": {\n        \"ignoreUnknown\": false,\n        \"includes\": [\n            \"**/*.ts\",\n            \"**/*.tsx\",\n            \"**/*.js\",\n            \"**/*.jsx\",\n            \"!**/dist/**/*\"\n        ]\n    },\n    \"formatter\": {\n        \"enabled\": true,\n        \"indentStyle\": \"tab\",\n        \"indentWidth\": 4,\n        \"lineWidth\": 120,\n        \"formatWithErrors\": true\n    },\n    \"assist\": {\n        \"actions\": {\n            \"source\": {\n                \"organizeImports\": {\n                    \"level\": \"on\",\n                    \"options\": {\n                        \"groups\": [\n                            \":BUN:\",\n                            \":NODE:\",\n                            [\n                                \"npm:*\",\n                                \"npm:*/**\"\n                            ],\n                            \":PACKAGE_WITH_PROTOCOL:\",\n                            \":URL:\",\n                            \":PACKAGE:\",\n                            [\n                                \"/src/*\",\n                                \"/src/**\"\n                            ],\n                            [\n                                \"/**\"\n                            ],\n                            [\n                                \"#*\",\n                                \"#*/**\"\n                            ],\n                            \":PATH:\"\n                        ]\n                    }\n                }\n            }\n        }\n    },\n    \"linter\": {\n        \"enabled\": true,\n        \"rules\": {\n            \"recommended\": true,\n            \"correctness\": {\n                \"useUniqueElementIds\": \"off\"\n            },\n            \"suspicious\": {\n                \"noExplicitAny\": \"off\",\n                \"noArrayIndexKey\": \"off\"\n            },\n            \"performance\": {\n                \"noDelete\": \"off\"\n            },\n            \"nursery\": \"off\",\n            \"a11y\": {\n                \"useSemanticElements\": \"off\",\n                \"useValidAnchor\": \"off\"\n            },\n            \"style\": {\n                \"noParameterAssign\": \"error\",\n                \"useAsConstAssertion\": \"error\",\n                \"useDefaultParameterLast\": \"error\",\n                \"useEnumInitializers\": \"error\",\n                \"useSelfClosingElements\": \"error\",\n                \"useSingleVarDeclarator\": \"error\",\n                \"noUnusedTemplateLiteral\": \"error\",\n                \"useNumberNamespace\": \"error\",\n                \"noInferrableTypes\": \"error\",\n                \"noUselessElse\": \"error\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "frontend/check-locales.cjs",
    "content": "#!/usr/bin/env node\n\n// This file does a few things to ensure that the Locales are present and valid:\n// - Ensures that the name of the locale exists in the language list\n// - Ensures that each locale contains the translations used in the application\n// - Ensures that there are no unused translations in the locale files\n// - Also checks the error messages returned by the backend\n\nconst allLocales = [\n  [\"en\", \"en-US\"],\n  [\"de\", \"de-DE\"],\n  [\"pt\", \"pt-PT\"],\n  [\"es\", \"es-ES\"],\n  [\"et\", \"et-EE\"],\n  [\"fr\", \"fr-FR\"],\n  [\"it\", \"it-IT\"],\n  [\"ja\", \"ja-JP\"],\n  [\"nl\", \"nl-NL\"],\n  [\"pl\", \"pl-PL\"],\n  [\"ru\", \"ru-RU\"],\n  [\"sk\", \"sk-SK\"],\n  [\"cs\", \"cs-CZ\"],\n  [\"vi\", \"vi-VN\"],\n  [\"zh\", \"zh-CN\"],\n  [\"ko\", \"ko-KR\"],\n  [\"bg\", \"bg-BG\"],\n  [\"id\", \"id-ID\"],\n  [\"tr\", \"tr-TR\"],\n  [\"hu\", \"hu-HU\"],\n  [\"no\", \"no-NO\"],\n];\n\nconst ignoreUnused = [/^.*$/];\n\nconst { spawnSync } = require(\"child_process\");\nconst fs = require(\"fs\");\n\nconst tmp = require(\"tmp\");\n\n// Parse backend errors\nconst BACKEND_ERRORS_FILE = \"../backend/internal/errors/errors.go\";\nconst BACKEND_ERRORS = [];\n/*\ntry {\n\tconst backendErrorsContent = fs.readFileSync(BACKEND_ERRORS_FILE, \"utf8\");\n\tconst backendErrorsContentRes = [\n\t\t...backendErrorsContent.matchAll(/(?:errors|eris)\\.New\\(\"([^\"]+)\"\\)/g),\n\t];\n\tbackendErrorsContentRes.map((item) => {\n\t\tBACKEND_ERRORS.push(\"error.\" + item[1]);\n\t\treturn null;\n\t});\n} catch (err) {\n\tconsole.log(\"\\x1b[31m%s\\x1b[0m\", err);\n\tprocess.exit(1);\n}\n*/\n\n// get all translations used in frontend code\nconst tmpobj = tmp.fileSync({ postfix: \".json\" });\nspawnSync(\"yarn\", [\"locale-extract\", \"--out-file\", tmpobj.name]);\n\nconst allLocalesInProject = require(tmpobj.name);\n\n// get list og language names and locales\nconst langList = require(\"./src/locale/src/lang-list.json\");\n\n// store a list of all validation errors\nconst allErrors = [];\nconst allWarnings = [];\nconst allKeys = [];\n\nconst checkLangList = (fullCode) => {\n  const key = \"locale-\" + fullCode;\n  if (typeof langList[key] === \"undefined\") {\n    allErrors.push(\"ERROR: `\" + key + \"` language does not exist in lang-list.json\");\n  }\n};\n\nconst compareLocale = (locale) => {\n  const projectLocaleKeys = Object.keys(allLocalesInProject);\n  // Check that locale contains the items used in the codebase\n  projectLocaleKeys.map((key) => {\n    if (typeof locale.data[key] === \"undefined\") {\n      allErrors.push(\"ERROR: `\" + locale[0] + \"` does not contain item: `\" + key + \"`\");\n    }\n    return null;\n  });\n  // Check that locale contains all error.* items\n  BACKEND_ERRORS.forEach((key) => {\n    if (typeof locale.data[key] === \"undefined\") {\n      allErrors.push(\"ERROR: `\" + locale[0] + \"` does not contain item: `\" + key + \"`\");\n    }\n    return null;\n  });\n\n  // Check that locale does not contain items not used in the codebase\n  const localeKeys = Object.keys(locale.data);\n  localeKeys.map((key) => {\n    let ignored = false;\n    ignoreUnused.map((regex) => {\n      if (key.match(regex)) {\n        ignored = true;\n      }\n      return null;\n    });\n\n    if (!ignored && typeof allLocalesInProject[key] === \"undefined\") {\n      // ensure this key doesn't exist in the backend errors either\n      if (!BACKEND_ERRORS.includes(key)) {\n        allErrors.push(\"ERROR: `\" + locale[0] + \"` contains unused item: `\" + key + \"`\");\n      }\n    }\n\n    // Add this key to allKeys\n    if (allKeys.indexOf(key) === -1) {\n      allKeys.push(key);\n    }\n    return null;\n  });\n};\n\n// Checks for any keys missing from this locale, that\n// have been defined in any other locales\nconst checkForMissing = (locale) => {\n  allKeys.forEach((key) => {\n    if (typeof locale.data[key] === \"undefined\") {\n      allWarnings.push(\"WARN: `\" + locale[0] + \"` does not contain item: `\" + key + \"`\");\n    }\n    return null;\n  });\n};\n\n// Local all locale data\nallLocales.map((locale, idx) => {\n  checkLangList(locale[1]);\n  allLocales[idx].data = require(\"./src/locale/src/\" + locale[0] + \".json\");\n  return null;\n});\n\n// Verify all locale data\nallLocales.map((locale) => {\n  compareLocale(locale);\n  checkForMissing(locale);\n  return null;\n});\n\nif (allErrors.length) {\n  allErrors.map((err) => {\n    console.log(\"\\x1b[31m%s\\x1b[0m\", err);\n    return null;\n  });\n}\nif (allWarnings.length) {\n  allWarnings.map((err) => {\n    console.log(\"\\x1b[33m%s\\x1b[0m\", err);\n    return null;\n  });\n}\n\nif (allErrors.length) {\n  process.exit(1);\n}\n\nconsole.log(\"\\x1b[32m%s\\x1b[0m\", \"Locale check passed\");\nprocess.exit(0);\n"
  },
  {
    "path": "frontend/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<title>Nginx Proxy Manager</title>\n\t\t<meta name=\"description\" content=\"Manage Nginx hosts with a simple, powerful interface\" />\n\t\t<link rel=\"preload\" href=\"/images/logo-no-text.svg\" as=\"image\" type=\"image/svg+xml\" fetchPriority=\"high\">\n\t\t<link\n\t\t\trel=\"apple-touch-icon\"\n\t\t\tsizes=\"180x180\"\n\t\t\thref=\"/images/favicon/apple-touch-icon.png\" />\n\t\t<link\n\t\t\trel=\"icon\"\n\t\t\ttype=\"image/png\"\n\t\t\tsizes=\"32x32\"\n\t\t\thref=\"/images/favicon/favicon-32x32.png\" />\n\t\t<link\n\t\t\trel=\"icon\"\n\t\t\ttype=\"image/png\"\n\t\t\tsizes=\"16x16\"\n\t\t\thref=\"/images/favicon/favicon-16x16.png\" />\n\t\t<link rel=\"manifest\" href=\"/images/favicon/site.webmanifest\" />\n\t\t<link\n\t\t\trel=\"mask-icon\"\n\t\t\thref=\"/images/favicon/safari-pinned-tab.svg\"\n\t\t\tcolor=\"#5bbad5\" />\n\t\t<link rel=\"shortcut icon\" href=\"/images/favicon/favicon.ico\" />\n\t\t<meta name=\"msapplication-TileColor\" content=\"#4a4a4a\" />\n\t\t<meta\n\t\t\tname=\"msapplication-config\"\n\t\t\tcontent=\"/images/favicon/browserconfig.xml\" />\n\t\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\t</head>\n\t<body>\n\t\t<noscript>You need to enable JavaScript to run this app.</noscript>\n\t\t<div id=\"root\"></div>\n\t\t<script type=\"module\" src=\"/src/main.tsx\"></script>\n\t\t<script>\n\t\t\tif (global === undefined) {\n\t\t\t\tvar global = window;\n\t\t\t}\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n\t\"name\": \"nginx-proxy-manager\",\n\t\"version\": \"2.0.0\",\n\t\"type\": \"module\",\n\t\"author\": \"Jamie Curnow <jc@jc21.com>\",\n\t\"license\": \"MIT\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite\",\n\t\t\"build\": \"tsc && vite build\",\n\t\t\"lint\": \"biome lint\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"prettier\": \"biome format --write ./src\",\n\t\t\"locale-extract\": \"formatjs extract 'src/**/*.tsx'\",\n\t\t\"locale-compile\": \"formatjs compile-folder src/locale/src src/locale/lang\",\n\t\t\"locale-sort\": \"./src/locale/scripts/locale-sort.sh\",\n\t\t\"test\": \"vitest\"\n\t},\n\t\"dependencies\": {\n\t\t\"@tabler/core\": \"^1.4.0\",\n\t\t\"@tabler/icons-react\": \"^3.38.0\",\n\t\t\"@tanstack/react-query\": \"^5.90.21\",\n\t\t\"@tanstack/react-table\": \"^8.21.3\",\n\t\t\"@uiw/react-textarea-code-editor\": \"^3.1.1\",\n\t\t\"classnames\": \"^2.5.1\",\n\t\t\"country-flag-icons\": \"^1.6.15\",\n\t\t\"date-fns\": \"^4.1.0\",\n\t\t\"ez-modal-react\": \"^1.0.5\",\n\t\t\"formik\": \"^2.4.9\",\n\t\t\"generate-password-browser\": \"^1.1.0\",\n\t\t\"humps\": \"^2.0.1\",\n\t\t\"query-string\": \"^9.3.1\",\n\t\t\"react\": \"^19.2.4\",\n\t\t\"react-bootstrap\": \"^2.10.10\",\n\t\t\"react-dom\": \"^19.2.4\",\n\t\t\"react-intl\": \"^8.1.3\",\n\t\t\"react-markdown\": \"^10.1.0\",\n\t\t\"react-router-dom\": \"^7.13.1\",\n\t\t\"react-select\": \"^5.10.2\",\n\t\t\"react-toastify\": \"^11.0.5\",\n\t\t\"rooks\": \"^9.5.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@biomejs/biome\": \"^2.4.5\",\n\t\t\"@formatjs/cli\": \"^6.13.0\",\n\t\t\"@tanstack/react-query-devtools\": \"^5.91.3\",\n\t\t\"@testing-library/dom\": \"^10.4.1\",\n\t\t\"@testing-library/jest-dom\": \"^6.9.1\",\n\t\t\"@testing-library/react\": \"^16.3.2\",\n\t\t\"@types/country-flag-icons\": \"^1.2.2\",\n\t\t\"@types/humps\": \"^2.0.6\",\n\t\t\"@types/react\": \"^19.2.14\",\n\t\t\"@types/react-dom\": \"^19.2.3\",\n\t\t\"@types/react-table\": \"^7.7.20\",\n\t\t\"@vitejs/plugin-react\": \"^5.1.4\",\n\t\t\"happy-dom\": \"^20.8.3\",\n\t\t\"postcss\": \"^8.5.8\",\n\t\t\"postcss-simple-vars\": \"^7.0.1\",\n\t\t\"sass\": \"^1.97.3\",\n\t\t\"tmp\": \"^0.2.5\",\n\t\t\"typescript\": \"5.9.3\",\n\t\t\"vite\": \"^7.3.1\",\n\t\t\"vite-plugin-checker\": \"^0.12.0\",\n\t\t\"vite-tsconfig-paths\": \"^6.1.1\",\n\t\t\"vitest\": \"^4.0.18\"\n\t}\n}\n"
  },
  {
    "path": "frontend/public/images/favicon/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/images/favicons/mstile-150x150.png\"/>\n            <TileColor>#333333</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "frontend/public/images/favicon/site.webmanifest",
    "content": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/images/favicons/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/images/favicons/android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "frontend/src/App.css",
    "content": ":root {\n\tcolor-scheme: light dark;\n}\n\n.light {\n\tcolor-scheme: light;\n}\n.dark {\n\tcolor-scheme: dark;\n}\n\n.modal-backdrop {\n\t--tblr-backdrop-opacity: 0.8 !important;\n}\n\n[data-bs-theme=\"dark\"] .modal-content {\n\t--tblr-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n[data-bs-theme=\"dark\"] .modal-backdrop {\n\t--tblr-backdrop-bg: #000 !important;\n\t--tblr-backdrop-opacity: 0.65 !important;\n}\n\n.domain-name {\n\tfont-family: monospace;\n}\n\n.mr-1 {\n\tmargin-right: 0.25rem;\n}\n.ml-1 {\n\tmargin-left: 0.25rem;\n}\n\n.react-select-container {\n\t.react-select__control  {\n\t\tcolor: var(--tblr-body-color);\n\t\tbackground-color: var(--tblr-bg-forms);\n\t\tborder: var(--tblr-border-width) solid var(--tblr-border-color);\n\n\t\t.react-select__input {\n\t\t\tcolor: var(--tblr-body-color) !important;\n\t\t}\n\n\t\t.react-select__single-value {\n\t\t\tcolor: var(--tblr-body-color);\n\t\t}\n\n\t\t.react-select__multi-value {\n\t\t\tborder: 1px solid var(--tblr-border-color);\n\t\t\tbackground-color: var(--tblr-bg-surface-tertiary);\n\t\t\tcolor: var(--tblr-secondary) !important;\n\n\t\t\t.react-select__multi-value__label {\n\t\t\t\tcolor: var(--tblr-secondary) !important;\n\t\t\t}\n\t\t}\n\t}\n\n\t.react-select__menu {\n\t\tbackground-color: var(--tblr-bg-forms);\n\n\t\t.react-select__option {\n\t\t\tbackground: rgba(var(--tblr-primary-rgb), .04);\n\t\t\tcolor: inherit !important;\n\t\t\t&.react-select__option--is-focused {\n\t\t\t\tbackground: rgba(var(--tblr-primary-rgb), .1);\n\t\t\t}\n\n\t\t\t&.react-select__option--is-focused.react-select__option--is-selected {\n\t\t\t\tbackground: rgba(var(--tblr-primary-rgb), .2);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.textareaMono {\n\tfont-family: 'Courier New', Courier, monospace !important;\n\tresize: vertical;\n}\n\nlabel.row {\n\tcursor: pointer;\n}\n\n.input-group-select {\n\tdisplay: flex;\n\talign-items: center;\n\tpadding: 0;\n\tfont-size: .875rem;\n\tfont-weight: 400;\n\tline-height: 1.25rem;\n\tcolor: var(--tblr-gray-500);\n\ttext-align: center;\n\twhite-space: nowrap;\n\tbackground-color: var(--tblr-bg-surface-secondary);\n\tborder: var(--tblr-border-width) solid var(--tblr-border-color);\n\tborder-radius: var(--tblr-border-radius);\n\n\t.form-select {\n\t\tborder: none;\n\t\tbackground-color: var(--tblr-bg-surface-secondary);\n\t\tborder-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius);\n\t}\n}\n\n/* Fix for dropdown menus being clipped by table-responsive containers. */\n.table-responsive .dropdown {\n\tposition: static;\n}\n\n/* Fix for Tabler scrollbar compensation */\n@media (min-width: 992px) {\n  :host, :root {\n    margin-left: 0;\n  }\n}"
  },
  {
    "path": "frontend/src/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport EasyModal from \"ez-modal-react\";\nimport { RawIntlProvider } from \"react-intl\";\nimport { ToastContainer } from \"react-toastify\";\nimport { AuthProvider, LocaleProvider, ThemeProvider } from \"src/context\";\nimport { intl } from \"src/locale\";\nimport Router from \"src/Router.tsx\";\n\n// Create a client\nconst queryClient = new QueryClient();\n\nfunction App() {\n\treturn (\n\t\t<RawIntlProvider value={intl}>\n\t\t\t<LocaleProvider>\n\t\t\t\t<ThemeProvider>\n\t\t\t\t\t<QueryClientProvider client={queryClient}>\n\t\t\t\t\t\t<AuthProvider>\n\t\t\t\t\t\t\t<EasyModal.Provider>\n\t\t\t\t\t\t\t\t<Router />\n\t\t\t\t\t\t\t</EasyModal.Provider>\n\t\t\t\t\t\t\t<ToastContainer\n\t\t\t\t\t\t\t\tposition=\"top-right\"\n\t\t\t\t\t\t\t\tautoClose={5000}\n\t\t\t\t\t\t\t\thideProgressBar={true}\n\t\t\t\t\t\t\t\tnewestOnTop={true}\n\t\t\t\t\t\t\t\tcloseOnClick={true}\n\t\t\t\t\t\t\t\trtl={false}\n\t\t\t\t\t\t\t\tcloseButton={false}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</AuthProvider>\n\t\t\t\t\t\t<ReactQueryDevtools buttonPosition=\"bottom-right\" position=\"right\" />\n\t\t\t\t\t</QueryClientProvider>\n\t\t\t\t</ThemeProvider>\n\t\t\t</LocaleProvider>\n\t\t</RawIntlProvider>\n\t);\n}\n\nexport default App;\n"
  },
  {
    "path": "frontend/src/Router.tsx",
    "content": "import { lazy, Suspense } from \"react\";\nimport { BrowserRouter, Route, Routes } from \"react-router-dom\";\nimport {\n\tErrorNotFound,\n\tLoadingPage,\n\tPage,\n\tSiteContainer,\n\tSiteFooter,\n\tSiteHeader,\n\tSiteMenu,\n\tUnhealthy,\n} from \"src/components\";\nimport { useAuthState } from \"src/context\";\nimport { useHealth } from \"src/hooks\";\n\nconst Setup = lazy(() => import(\"src/pages/Setup\"));\nconst Login = lazy(() => import(\"src/pages/Login\"));\nconst Dashboard = lazy(() => import(\"src/pages/Dashboard\"));\nconst Settings = lazy(() => import(\"src/pages/Settings\"));\nconst Certificates = lazy(() => import(\"src/pages/Certificates\"));\nconst Access = lazy(() => import(\"src/pages/Access\"));\nconst AuditLog = lazy(() => import(\"src/pages/AuditLog\"));\nconst Users = lazy(() => import(\"src/pages/Users\"));\nconst ProxyHosts = lazy(() => import(\"src/pages/Nginx/ProxyHosts\"));\nconst RedirectionHosts = lazy(() => import(\"src/pages/Nginx/RedirectionHosts\"));\nconst DeadHosts = lazy(() => import(\"src/pages/Nginx/DeadHosts\"));\nconst Streams = lazy(() => import(\"src/pages/Nginx/Streams\"));\n\nfunction Router() {\n\tconst health = useHealth();\n\tconst { authenticated } = useAuthState();\n\n\tif (health.isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (health.isError || health.data?.status !== \"OK\") {\n\t\treturn <Unhealthy />;\n\t}\n\n\tif (!health.data?.setup) {\n\t\treturn <Setup />;\n\t}\n\n\tif (!authenticated) {\n\t\treturn (\n\t\t\t<Suspense fallback={<LoadingPage />}>\n\t\t\t\t<Login />\n\t\t\t</Suspense>\n\t\t);\n\t}\n\n\treturn (\n\t\t<BrowserRouter>\n\t\t\t<Page>\n\t\t\t\t<div>\n\t\t\t\t\t<SiteHeader />\n\t\t\t\t\t<SiteMenu />\n\t\t\t\t</div>\n\t\t\t\t<SiteContainer>\n\t\t\t\t\t<Suspense fallback={<LoadingPage noLogo />}>\n\t\t\t\t\t\t<Routes>\n\t\t\t\t\t\t\t<Route path=\"*\" element={<ErrorNotFound />} />\n\t\t\t\t\t\t\t<Route path=\"/certificates\" element={<Certificates />} />\n\t\t\t\t\t\t\t<Route path=\"/access\" element={<Access />} />\n\t\t\t\t\t\t\t<Route path=\"/audit-log\" element={<AuditLog />} />\n\t\t\t\t\t\t\t<Route path=\"/settings\" element={<Settings />} />\n\t\t\t\t\t\t\t<Route path=\"/users\" element={<Users />} />\n\t\t\t\t\t\t\t<Route path=\"/nginx/proxy\" element={<ProxyHosts />} />\n\t\t\t\t\t\t\t<Route path=\"/nginx/redirection\" element={<RedirectionHosts />} />\n\t\t\t\t\t\t\t<Route path=\"/nginx/404\" element={<DeadHosts />} />\n\t\t\t\t\t\t\t<Route path=\"/nginx/stream\" element={<Streams />} />\n\t\t\t\t\t\t\t<Route path=\"/\" element={<Dashboard />} />\n\t\t\t\t\t\t</Routes>\n\t\t\t\t\t</Suspense>\n\t\t\t\t</SiteContainer>\n\t\t\t\t<SiteFooter />\n\t\t\t</Page>\n\t\t</BrowserRouter>\n\t);\n}\n\nexport default Router;\n"
  },
  {
    "path": "frontend/src/api/backend/base.ts",
    "content": "import { QueryClient } from \"@tanstack/react-query\";\nimport { camelizeKeys, decamelize, decamelizeKeys } from \"humps\";\nimport queryString, { type StringifiableRecord } from \"query-string\";\nimport AuthStore from \"src/modules/AuthStore\";\n\nconst queryClient = new QueryClient();\nconst contentTypeHeader = \"Content-Type\";\n\ninterface BuildUrlArgs {\n\turl: string;\n\tparams?: StringifiableRecord;\n}\n\nfunction decamelizeParams(params?: StringifiableRecord): StringifiableRecord | undefined {\n\tif (!params) {\n\t\treturn undefined;\n\t}\n\tconst result: StringifiableRecord = {};\n\tfor (const [key, value] of Object.entries(params)) {\n\t\tresult[decamelize(key)] = value;\n\t}\n\n\treturn result;\n}\n\nfunction buildUrl({ url, params }: BuildUrlArgs) {\n\tconst endpoint = url.replace(/^\\/|\\/$/g, \"\");\n\tconst baseUrl = `/api/${endpoint}`;\n\tconst apiUrl = queryString.stringifyUrl({\n\t\turl: baseUrl,\n\t\tquery: decamelizeParams(params),\n\t});\n\treturn apiUrl;\n}\n\nfunction buildAuthHeader(): Record<string, string> | undefined {\n\tif (AuthStore.token) {\n\t\treturn { Authorization: `Bearer ${AuthStore.token.token}` };\n\t}\n\treturn {};\n}\n\nfunction buildBody(data?: Record<string, any>): string | undefined {\n\tif (data) {\n\t\treturn JSON.stringify(decamelizeKeys(data));\n\t}\n}\n\nasync function processResponse(response: Response) {\n\tconst payload = await response.json();\n\tif (!response.ok) {\n\t\tif (response.status === 401) {\n\t\t\t// Force logout user and reload the page if Unauthorized\n\t\t\tAuthStore.clear();\n\t\t\tqueryClient.clear();\n\t\t\twindow.location.reload();\n\t\t}\n\t\tthrow new Error(\n\t\t\ttypeof payload.error.messageI18n !== \"undefined\" ? payload.error.messageI18n : payload.error.message,\n\t\t);\n\t}\n\treturn camelizeKeys(payload) as any;\n}\n\ninterface GetArgs {\n\turl: string;\n\tparams?: queryString.StringifiableRecord;\n}\n\nasync function baseGet({ url, params }: GetArgs, abortController?: AbortController) {\n\tconst apiUrl = buildUrl({ url, params });\n\tconst method = \"GET\";\n\tconst headers = buildAuthHeader();\n\tconst signal = abortController?.signal;\n\tconst response = await fetch(apiUrl, { method, headers, signal });\n\treturn response;\n}\n\nexport async function get(args: GetArgs, abortController?: AbortController) {\n\treturn processResponse(await baseGet(args, abortController));\n}\n\nexport async function download({ url, params }: GetArgs, filename = \"download.file\") {\n\tconst headers = buildAuthHeader();\n\tconst res = await fetch(buildUrl({ url, params }), { headers });\n\tconst bl = await res.blob();\n\tconst u = window.URL.createObjectURL(bl);\n\tconst a = document.createElement(\"a\");\n\ta.href = u;\n\ta.download = filename;\n\ta.click();\n\twindow.URL.revokeObjectURL(url);\n}\n\ninterface PostArgs {\n\turl: string;\n\tparams?: queryString.StringifiableRecord;\n\tdata?: any;\n\tnoAuth?: boolean;\n}\n\nexport async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) {\n\tconst apiUrl = buildUrl({ url, params });\n\tconst method = \"POST\";\n\n\tlet headers: Record<string, string> = {};\n\tif (!noAuth) {\n\t\theaders = {\n\t\t\t...buildAuthHeader(),\n\t\t};\n\t}\n\n\tlet body: string | FormData | undefined;\n\t// Check if the data is an instance of FormData\n\t// If data is FormData, let the browser set the Content-Type header\n\tif (data instanceof FormData) {\n\t\tbody = data;\n\t} else {\n\t\t// If data is JSON, set the Content-Type header to 'application/json'\n\t\theaders = {\n\t\t\t...headers,\n\t\t\t[contentTypeHeader]: \"application/json\",\n\t\t};\n\t\tbody = buildBody(data);\n\t}\n\n\tconst signal = abortController?.signal;\n\tconst response = await fetch(apiUrl, { method, headers, body, signal });\n\treturn processResponse(response);\n}\n\ninterface PutArgs {\n\turl: string;\n\tparams?: queryString.StringifiableRecord;\n\tdata?: Record<string, any>;\n}\nexport async function put({ url, params, data }: PutArgs, abortController?: AbortController) {\n\tconst apiUrl = buildUrl({ url, params });\n\tconst method = \"PUT\";\n\tconst headers = {\n\t\t...buildAuthHeader(),\n\t\t[contentTypeHeader]: \"application/json\",\n\t};\n\tconst signal = abortController?.signal;\n\tconst body = buildBody(data);\n\tconst response = await fetch(apiUrl, { method, headers, body, signal });\n\treturn processResponse(response);\n}\n\ninterface DeleteArgs {\n\turl: string;\n\tparams?: queryString.StringifiableRecord;\n}\nexport async function del({ url, params }: DeleteArgs, abortController?: AbortController) {\n\tconst apiUrl = buildUrl({ url, params });\n\tconst method = \"DELETE\";\n\tconst headers = {\n\t\t...buildAuthHeader(),\n\t};\n\tconst signal = abortController?.signal;\n\tconst response = await fetch(apiUrl, { method, headers, signal });\n\treturn processResponse(response);\n}\n"
  },
  {
    "path": "frontend/src/api/backend/checkVersion.ts",
    "content": "import * as api from \"./base\";\nimport type { VersionCheckResponse } from \"./responseTypes\";\n\nexport async function checkVersion(): Promise<VersionCheckResponse> {\n\treturn await api.get({\n\t\turl: \"/version/check\",\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createAccessList.ts",
    "content": "import * as api from \"./base\";\nimport type { AccessList } from \"./models\";\n\nexport async function createAccessList(item: AccessList): Promise<AccessList> {\n\treturn await api.post({\n\t\turl: \"/nginx/access-lists\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createCertificate.ts",
    "content": "import * as api from \"./base\";\nimport type { Certificate } from \"./models\";\n\nexport async function createCertificate(item: Certificate): Promise<Certificate> {\n\treturn await api.post({\n\t\turl: \"/nginx/certificates\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createDeadHost.ts",
    "content": "import * as api from \"./base\";\nimport type { DeadHost } from \"./models\";\n\nexport async function createDeadHost(item: DeadHost): Promise<DeadHost> {\n\treturn await api.post({\n\t\turl: \"/nginx/dead-hosts\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createProxyHost.ts",
    "content": "import * as api from \"./base\";\nimport type { ProxyHost } from \"./models\";\n\nexport async function createProxyHost(item: ProxyHost): Promise<ProxyHost> {\n\treturn await api.post({\n\t\turl: \"/nginx/proxy-hosts\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createRedirectionHost.ts",
    "content": "import * as api from \"./base\";\nimport type { RedirectionHost } from \"./models\";\n\nexport async function createRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> {\n\treturn await api.post({\n\t\turl: \"/nginx/redirection-hosts\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createStream.ts",
    "content": "import * as api from \"./base\";\nimport type { Stream } from \"./models\";\n\nexport async function createStream(item: Stream): Promise<Stream> {\n\treturn await api.post({\n\t\turl: \"/nginx/streams\",\n\t\tdata: item,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/createUser.ts",
    "content": "import * as api from \"./base\";\nimport type { User } from \"./models\";\n\nexport interface AuthOptions {\n\ttype: string;\n\tsecret: string;\n}\n\nexport interface NewUser {\n\tname: string;\n\tnickname: string;\n\temail: string;\n\tisDisabled?: boolean;\n\tauth?: AuthOptions;\n\troles?: string[];\n}\n\nexport async function createUser(item: NewUser, noAuth?: boolean): Promise<User> {\n\treturn await api.post({\n\t\turl: \"/users\",\n\t\tdata: item,\n\t\tnoAuth,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteAccessList.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteAccessList(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/access-lists/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteCertificate.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteCertificate(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/certificates/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteDeadHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteDeadHost(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/dead-hosts/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteProxyHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteProxyHost(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/proxy-hosts/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteRedirectionHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteRedirectionHost(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/redirection-hosts/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteStream.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteStream(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/nginx/streams/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/deleteUser.ts",
    "content": "import * as api from \"./base\";\n\nexport async function deleteUser(id: number): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/users/${id}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/downloadCertificate.ts",
    "content": "import * as api from \"./base\";\n\nexport async function downloadCertificate(id: number): Promise<void> {\n\tawait api.download(\n\t\t{\n\t\t\turl: `/nginx/certificates/${id}/download`,\n\t\t},\n\t\t`certificate-${id}.zip`,\n\t);\n}\n"
  },
  {
    "path": "frontend/src/api/backend/expansions.ts",
    "content": "export type AccessListExpansion = \"owner\" | \"items\" | \"clients\";\nexport type AuditLogExpansion = \"user\";\nexport type CertificateExpansion = \"owner\" | \"proxy_hosts\" | \"redirection_hosts\" | \"dead_hosts\" | \"streams\";\nexport type HostExpansion = \"owner\" | \"certificate\";\nexport type ProxyHostExpansion = \"owner\" | \"access_list\" | \"certificate\";\nexport type UserExpansion = \"permissions\";\n"
  },
  {
    "path": "frontend/src/api/backend/getAccessList.ts",
    "content": "import * as api from \"./base\";\nimport type { AccessListExpansion } from \"./expansions\";\nimport type { AccessList } from \"./models\";\n\nexport async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise<AccessList> {\n\treturn await api.get({\n\t\turl: `/nginx/access-lists/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getAccessLists.ts",
    "content": "import * as api from \"./base\";\nimport type { AccessListExpansion } from \"./expansions\";\nimport type { AccessList } from \"./models\";\n\nexport async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/access-lists\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getAuditLog.ts",
    "content": "import * as api from \"./base\";\nimport type { AuditLogExpansion } from \"./expansions\";\nimport type { AuditLog } from \"./models\";\n\nexport async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> {\n\treturn await api.get({\n\t\turl: `/audit-log/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getAuditLogs.ts",
    "content": "import * as api from \"./base\";\nimport type { AuditLogExpansion } from \"./expansions\";\nimport type { AuditLog } from \"./models\";\n\nexport async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> {\n\treturn await api.get({\n\t\turl: \"/audit-log\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getCertificate.ts",
    "content": "import * as api from \"./base\";\nimport type { CertificateExpansion } from \"./expansions\";\nimport type { Certificate } from \"./models\";\n\nexport async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise<Certificate> {\n\treturn await api.get({\n\t\turl: `/nginx/certificates/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getCertificateDNSProviders.ts",
    "content": "import * as api from \"./base\";\nimport type { DNSProvider } from \"./models\";\n\nexport async function getCertificateDNSProviders(params = {}): Promise<DNSProvider[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/certificates/dns-providers\",\n\t\tparams,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getCertificates.ts",
    "content": "import * as api from \"./base\";\nimport type { CertificateExpansion } from \"./expansions\";\nimport type { Certificate } from \"./models\";\n\nexport async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/certificates\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getDeadHost.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { DeadHost } from \"./models\";\n\nexport async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise<DeadHost> {\n\treturn await api.get({\n\t\turl: `/nginx/dead-hosts/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getDeadHosts.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { DeadHost } from \"./models\";\n\nexport async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise<DeadHost[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/dead-hosts\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getHealth.ts",
    "content": "import * as api from \"./base\";\nimport type { HealthResponse } from \"./responseTypes\";\n\nexport async function getHealth(): Promise<HealthResponse> {\n\treturn await api.get({\n\t\turl: \"/\",\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getHostsReport.ts",
    "content": "import * as api from \"./base\";\n\nexport async function getHostsReport(): Promise<Record<string, number>> {\n\treturn await api.get({\n\t\turl: \"/reports/hosts\",\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getProxyHost.ts",
    "content": "import * as api from \"./base\";\nimport type { ProxyHostExpansion } from \"./expansions\";\nimport type { ProxyHost } from \"./models\";\n\nexport async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost> {\n\treturn await api.get({\n\t\turl: `/nginx/proxy-hosts/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getProxyHosts.ts",
    "content": "import * as api from \"./base\";\nimport type { ProxyHostExpansion } from \"./expansions\";\nimport type { ProxyHost } from \"./models\";\n\nexport async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/proxy-hosts\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getRedirectionHost.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { RedirectionHost } from \"./models\";\n\nexport async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise<RedirectionHost> {\n\treturn await api.get({\n\t\turl: `/nginx/redirection-hosts/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getRedirectionHosts.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { RedirectionHost } from \"./models\";\n\nexport async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise<RedirectionHost[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/redirection-hosts\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getSetting.ts",
    "content": "import * as api from \"./base\";\nimport type { Setting } from \"./models\";\n\nexport async function getSetting(id: string, expand?: string[], params = {}): Promise<Setting> {\n\treturn await api.get({\n\t\turl: `/settings/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getSettings.ts",
    "content": "import * as api from \"./base\";\nimport type { Setting } from \"./models\";\n\nexport async function getSettings(expand?: string[], params = {}): Promise<Setting[]> {\n\treturn await api.get({\n\t\turl: \"/settings\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getStream.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { Stream } from \"./models\";\n\nexport async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise<Stream> {\n\treturn await api.get({\n\t\turl: `/nginx/streams/${id}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getStreams.ts",
    "content": "import * as api from \"./base\";\nimport type { HostExpansion } from \"./expansions\";\nimport type { Stream } from \"./models\";\n\nexport async function getStreams(expand?: HostExpansion[], params = {}): Promise<Stream[]> {\n\treturn await api.get({\n\t\turl: \"/nginx/streams\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getToken.ts",
    "content": "import * as api from \"./base\";\nimport type { TokenResponse, TwoFactorChallengeResponse } from \"./responseTypes\";\n\nexport type LoginResponse = TokenResponse | TwoFactorChallengeResponse;\n\nexport function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse {\n\treturn \"requires2fa\" in response && response.requires2fa === true;\n}\n\nexport async function getToken(identity: string, secret: string): Promise<LoginResponse> {\n\treturn await api.post({\n\t\turl: \"/tokens\",\n\t\tdata: { identity, secret },\n\t});\n}\n\nexport async function verify2FA(challengeToken: string, code: string): Promise<TokenResponse> {\n\treturn await api.post({\n\t\turl: \"/tokens/2fa\",\n\t\tdata: { challengeToken, code },\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getUser.ts",
    "content": "import * as api from \"./base\";\nimport type { UserExpansion } from \"./expansions\";\nimport type { User } from \"./models\";\n\nexport async function getUser(id: number | string = \"me\", expand?: UserExpansion[], params = {}): Promise<User> {\n\tconst userId = id ? id : \"me\";\n\treturn await api.get({\n\t\turl: `/users/${userId}`,\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/getUsers.ts",
    "content": "import * as api from \"./base\";\nimport type { UserExpansion } from \"./expansions\";\nimport type { User } from \"./models\";\n\nexport async function getUsers(expand?: UserExpansion[], params = {}): Promise<User[]> {\n\treturn await api.get({\n\t\turl: \"/users\",\n\t\tparams: {\n\t\t\texpand: expand?.join(\",\"),\n\t\t\t...params,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/helpers.ts",
    "content": "import { decamelize } from \"humps\";\n\n/**\n * This will convert a react-table sort object into\n * a string that the backend api likes:\n *   name.asc,id.desc\n */\nexport function tableSortToAPI(sortBy: any): string | undefined {\n\tif (sortBy?.length > 0) {\n\t\tconst strs: string[] = [];\n\t\tsortBy.map((item: any) => {\n\t\t\tstrs.push(`${decamelize(item.id)}.${item.desc ? \"desc\" : \"asc\"}`);\n\t\t\treturn undefined;\n\t\t});\n\t\treturn strs.join(\",\");\n\t}\n\treturn;\n}\n\n/**\n * This will convert a react-table filters object into\n * a string that the backend api likes:\n *   name:contains=jam\n */\nexport function tableFiltersToAPI(filters: any[]): { [key: string]: string } {\n\tconst items: { [key: string]: string } = {};\n\tif (filters?.length > 0) {\n\t\tfilters.map((item: any) => {\n\t\t\titems[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value;\n\t\t\treturn undefined;\n\t\t});\n\t}\n\treturn items;\n}\n\n/**\n * Builds a filters object by removing entries with undefined, null, or empty string values.\n *\n */\nexport function buildFilters(filters?: Record<string, string | boolean | undefined | null>) {\n\tif (!filters) {\n\t\treturn filters;\n\t}\n\tconst result: Record<string, string> = {};\n\tfor (const key in filters) {\n\t\tconst value = filters[key];\n\t\t// If the value is undefined, null, or an empty string, skip it\n\t\tif (value === undefined || value === null || value === \"\") {\n\t\t\tcontinue;\n\t\t}\n\t\tresult[key] = value.toString();\n\t}\n\treturn result;\n}\n"
  },
  {
    "path": "frontend/src/api/backend/index.ts",
    "content": "export * from \"./checkVersion\";\nexport * from \"./createAccessList\";\nexport * from \"./createCertificate\";\nexport * from \"./createDeadHost\";\nexport * from \"./createProxyHost\";\nexport * from \"./createRedirectionHost\";\nexport * from \"./createStream\";\nexport * from \"./createUser\";\nexport * from \"./deleteAccessList\";\nexport * from \"./deleteCertificate\";\nexport * from \"./deleteDeadHost\";\nexport * from \"./deleteProxyHost\";\nexport * from \"./deleteRedirectionHost\";\nexport * from \"./deleteStream\";\nexport * from \"./deleteUser\";\nexport * from \"./downloadCertificate\";\nexport * from \"./expansions\";\nexport * from \"./getAccessList\";\nexport * from \"./getAccessLists\";\nexport * from \"./getAuditLog\";\nexport * from \"./getAuditLogs\";\nexport * from \"./getCertificate\";\nexport * from \"./getCertificateDNSProviders\";\nexport * from \"./getCertificates\";\nexport * from \"./getDeadHost\";\nexport * from \"./getDeadHosts\";\nexport * from \"./getHealth\";\nexport * from \"./getHostsReport\";\nexport * from \"./getProxyHost\";\nexport * from \"./getProxyHosts\";\nexport * from \"./getRedirectionHost\";\nexport * from \"./getRedirectionHosts\";\nexport * from \"./getSetting\";\nexport * from \"./getSettings\";\nexport * from \"./getStream\";\nexport * from \"./getStreams\";\nexport * from \"./getToken\";\nexport * from \"./getUser\";\nexport * from \"./getUsers\";\nexport * from \"./helpers\";\nexport * from \"./loginAsUser\";\nexport * from \"./models\";\nexport * from \"./refreshToken\";\nexport * from \"./renewCertificate\";\nexport * from \"./responseTypes\";\nexport * from \"./setPermissions\";\nexport * from \"./testHttpCertificate\";\nexport * from \"./toggleDeadHost\";\nexport * from \"./toggleProxyHost\";\nexport * from \"./toggleRedirectionHost\";\nexport * from \"./toggleStream\";\nexport * from \"./toggleUser\";\nexport * from \"./updateAccessList\";\nexport * from \"./updateAuth\";\nexport * from \"./updateDeadHost\";\nexport * from \"./updateProxyHost\";\nexport * from \"./updateRedirectionHost\";\nexport * from \"./updateSetting\";\nexport * from \"./updateStream\";\nexport * from \"./updateUser\";\nexport * from \"./uploadCertificate\";\nexport * from \"./validateCertificate\";\nexport * from \"./twoFactor\";\n"
  },
  {
    "path": "frontend/src/api/backend/loginAsUser.ts",
    "content": "import * as api from \"./base\";\nimport type { LoginAsTokenResponse } from \"./responseTypes\";\n\nexport async function loginAsUser(id: number): Promise<LoginAsTokenResponse> {\n\treturn await api.post({\n\t\turl: `/users/${id}/login`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/models.ts",
    "content": "export interface AppVersion {\n\tmajor: number;\n\tminor: number;\n\trevision: number;\n}\n\nexport interface UserPermissions {\n\tid?: number;\n\tcreatedOn?: string;\n\tmodifiedOn?: string;\n\tuserId?: number;\n\tvisibility: string;\n\tproxyHosts: string;\n\tredirectionHosts: string;\n\tdeadHosts: string;\n\tstreams: string;\n\taccessLists: string;\n\tcertificates: string;\n}\n\nexport interface User {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\tisDisabled: boolean;\n\temail: string;\n\tname: string;\n\tnickname: string;\n\tavatar: string;\n\troles: string[];\n\tpermissions?: UserPermissions;\n}\n\nexport interface AuditLog {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\tuserId: number;\n\tobjectType: string;\n\tobjectId: number;\n\taction: string;\n\tmeta: Record<string, any>;\n\t// Expansions:\n\tuser?: User;\n}\n\nexport interface AccessList {\n\tid?: number;\n\tcreatedOn?: string;\n\tmodifiedOn?: string;\n\townerUserId: number;\n\tname: string;\n\tmeta: Record<string, any>;\n\tsatisfyAny: boolean;\n\tpassAuth: boolean;\n\tproxyHostCount?: number;\n\t// Expansions:\n\towner?: User;\n\titems?: AccessListItem[];\n\tclients?: AccessListClient[];\n}\n\nexport interface AccessListItem {\n\tid?: number;\n\tcreatedOn?: string;\n\tmodifiedOn?: string;\n\taccessListId?: number;\n\tusername: string;\n\tpassword: string;\n\tmeta?: Record<string, any>;\n\thint?: string;\n}\n\nexport type AccessListClient = {\n\tid?: number;\n\tcreatedOn?: string;\n\tmodifiedOn?: string;\n\taccessListId?: number;\n\taddress: string;\n\tdirective: \"allow\" | \"deny\";\n\tmeta?: Record<string, any>;\n};\n\nexport interface Certificate {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\townerUserId: number;\n\tprovider: string;\n\tniceName: string;\n\tdomainNames: string[];\n\texpiresOn: string;\n\tmeta: Record<string, any>;\n\towner?: User;\n\tproxyHosts?: ProxyHost[];\n\tdeadHosts?: DeadHost[];\n\tredirectionHosts?: RedirectionHost[];\n}\n\nexport interface ProxyLocation {\n\tpath: string;\n\tadvancedConfig: string;\n\tforwardScheme: string;\n\tforwardHost: string;\n\tforwardPort: number;\n}\n\nexport interface ProxyHost {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\townerUserId: number;\n\tdomainNames: string[];\n\tforwardScheme: string;\n\tforwardHost: string;\n\tforwardPort: number;\n\taccessListId: number;\n\tcertificateId: number;\n\tsslForced: boolean;\n\tcachingEnabled: boolean;\n\tblockExploits: boolean;\n\tadvancedConfig: string;\n\tmeta: Record<string, any>;\n\tallowWebsocketUpgrade: boolean;\n\thttp2Support: boolean;\n\tenabled: boolean;\n\tlocations?: ProxyLocation[];\n\thstsEnabled: boolean;\n\thstsSubdomains: boolean;\n\ttrustForwardedProto: boolean;\n\t// Expansions:\n\towner?: User;\n\taccessList?: AccessList;\n\tcertificate?: Certificate;\n}\n\nexport interface DeadHost {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\townerUserId: number;\n\tdomainNames: string[];\n\tcertificateId: number;\n\tsslForced: boolean;\n\tadvancedConfig: string;\n\tmeta: Record<string, any>;\n\thttp2Support: boolean;\n\tenabled: boolean;\n\thstsEnabled: boolean;\n\thstsSubdomains: boolean;\n\t// Expansions:\n\towner?: User;\n\tcertificate?: Certificate;\n}\n\nexport interface RedirectionHost {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\townerUserId: number;\n\tdomainNames: string[];\n\tforwardDomainName: string;\n\tpreservePath: boolean;\n\tcertificateId: number;\n\tsslForced: boolean;\n\tblockExploits: boolean;\n\tadvancedConfig: string;\n\tmeta: Record<string, any>;\n\thttp2Support: boolean;\n\tforwardScheme: string;\n\tforwardHttpCode: number;\n\tenabled: boolean;\n\thstsEnabled: boolean;\n\thstsSubdomains: boolean;\n\t// Expansions:\n\towner?: User;\n\tcertificate?: Certificate;\n}\n\nexport interface Stream {\n\tid: number;\n\tcreatedOn: string;\n\tmodifiedOn: string;\n\townerUserId: number;\n\tincomingPort: number;\n\tforwardingHost: string;\n\tforwardingPort: number;\n\ttcpForwarding: boolean;\n\tudpForwarding: boolean;\n\tmeta: Record<string, any>;\n\tenabled: boolean;\n\tcertificateId: number;\n\t// Expansions:\n\towner?: User;\n\tcertificate?: Certificate;\n}\n\nexport interface Setting {\n\tid: string;\n\tname?: string;\n\tdescription?: string;\n\tvalue: string;\n\tmeta?: Record<string, any>;\n}\n\nexport interface DNSProvider {\n\tid: string;\n\tname: string;\n\tcredentials: string;\n}\n"
  },
  {
    "path": "frontend/src/api/backend/refreshToken.ts",
    "content": "import * as api from \"./base\";\nimport type { TokenResponse } from \"./responseTypes\";\n\nexport async function refreshToken(): Promise<TokenResponse> {\n\treturn await api.get({\n\t\turl: \"/tokens\",\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/renewCertificate.ts",
    "content": "import * as api from \"./base\";\nimport type { Certificate } from \"./models\";\n\nexport async function renewCertificate(id: number): Promise<Certificate> {\n\treturn await api.post({\n\t\turl: `/nginx/certificates/${id}/renew`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/responseTypes.ts",
    "content": "import type { AppVersion, User } from \"./models\";\n\nexport interface HealthResponse {\n\tstatus: string;\n\tversion: AppVersion;\n\tsetup: boolean;\n}\n\nexport interface TokenResponse {\n\texpires: number;\n\ttoken: string;\n}\n\nexport interface ValidatedCertificateResponse {\n\tcertificate: Record<string, any>;\n\tcertificateKey: boolean;\n}\n\nexport interface LoginAsTokenResponse extends TokenResponse {\n\tuser: User;\n}\n\nexport interface VersionCheckResponse {\n\tcurrent: string | null;\n\tlatest: string | null;\n\tupdateAvailable: boolean;\n}\n\nexport interface TwoFactorChallengeResponse {\n\trequires2fa: boolean;\n\tchallengeToken: string;\n}\n\nexport interface TwoFactorStatusResponse {\n\tenabled: boolean;\n\tbackupCodesRemaining: number;\n}\n\nexport interface TwoFactorSetupResponse {\n\tsecret: string;\n\totpauthUrl: string;\n}\n\nexport interface TwoFactorEnableResponse {\n\tbackupCodes: string[];\n}\n"
  },
  {
    "path": "frontend/src/api/backend/setPermissions.ts",
    "content": "import * as api from \"./base\";\nimport type { UserPermissions } from \"./models\";\n\nexport async function setPermissions(userId: number, data: UserPermissions): Promise<boolean> {\n\t// Remove readonly fields\n\treturn await api.put({\n\t\turl: `/users/${userId}/permissions`,\n\t\tdata,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/testHttpCertificate.ts",
    "content": "import * as api from \"./base\";\n\nexport async function testHttpCertificate(domains: string[]): Promise<Record<string, string>> {\n\treturn await api.post({\n\t\turl: \"/nginx/certificates/test-http\",\n\t\tdata: {\n\t\t\tdomains,\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/toggleDeadHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function toggleDeadHost(id: number, enabled: boolean): Promise<boolean> {\n\treturn await api.post({\n\t\turl: `/nginx/dead-hosts/${id}/${enabled ? \"enable\" : \"disable\"}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/toggleProxyHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function toggleProxyHost(id: number, enabled: boolean): Promise<boolean> {\n\treturn await api.post({\n\t\turl: `/nginx/proxy-hosts/${id}/${enabled ? \"enable\" : \"disable\"}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/toggleRedirectionHost.ts",
    "content": "import * as api from \"./base\";\n\nexport async function toggleRedirectionHost(id: number, enabled: boolean): Promise<boolean> {\n\treturn await api.post({\n\t\turl: `/nginx/redirection-hosts/${id}/${enabled ? \"enable\" : \"disable\"}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/toggleStream.ts",
    "content": "import * as api from \"./base\";\n\nexport async function toggleStream(id: number, enabled: boolean): Promise<boolean> {\n\treturn await api.post({\n\t\turl: `/nginx/streams/${id}/${enabled ? \"enable\" : \"disable\"}`,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/toggleUser.ts",
    "content": "import type { User } from \"./models\";\nimport { updateUser } from \"./updateUser\";\n\nexport async function toggleUser(id: number, enabled: boolean): Promise<boolean> {\n\tawait updateUser({\n\t\tid,\n\t\tisDisabled: !enabled,\n\t} as User);\n\treturn true;\n}\n"
  },
  {
    "path": "frontend/src/api/backend/twoFactor.ts",
    "content": "import * as api from \"./base\";\nimport type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from \"./responseTypes\";\n\nexport async function get2FAStatus(userId: number | \"me\"): Promise<TwoFactorStatusResponse> {\n\treturn await api.get({\n\t\turl: `/users/${userId}/2fa`,\n\t});\n}\n\nexport async function start2FASetup(userId: number | \"me\"): Promise<TwoFactorSetupResponse> {\n\treturn await api.post({\n\t\turl: `/users/${userId}/2fa`,\n\t});\n}\n\nexport async function enable2FA(userId: number | \"me\", code: string): Promise<TwoFactorEnableResponse> {\n\treturn await api.post({\n\t\turl: `/users/${userId}/2fa/enable`,\n\t\tdata: { code },\n\t});\n}\n\nexport async function disable2FA(userId: number | \"me\", code: string): Promise<boolean> {\n\treturn await api.del({\n\t\turl: `/users/${userId}/2fa`,\n\t\tparams: {\n\t\t\tcode,\n\t\t},\n\t});\n}\n\nexport async function regenerateBackupCodes(userId: number | \"me\", code: string): Promise<TwoFactorEnableResponse> {\n\treturn await api.post({\n\t\turl: `/users/${userId}/2fa/backup-codes`,\n\t\tdata: { code },\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateAccessList.ts",
    "content": "import * as api from \"./base\";\nimport type { AccessList } from \"./models\";\n\nexport async function updateAccessList(item: AccessList): Promise<AccessList> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/nginx/access-lists/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateAuth.ts",
    "content": "import * as api from \"./base\";\nimport type { User } from \"./models\";\n\nexport async function updateAuth(userId: number | \"me\", newPassword: string, current?: string): Promise<User> {\n\tconst data = {\n\t\ttype: \"password\",\n\t\tcurrent: current,\n\t\tsecret: newPassword,\n\t};\n\tif (userId === \"me\") {\n\t\tdata.current = current;\n\t}\n\n\treturn await api.put({\n\t\turl: `/users/${userId}/auth`,\n\t\tdata,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateDeadHost.ts",
    "content": "import * as api from \"./base\";\nimport type { DeadHost } from \"./models\";\n\nexport async function updateDeadHost(item: DeadHost): Promise<DeadHost> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/nginx/dead-hosts/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateProxyHost.ts",
    "content": "import * as api from \"./base\";\nimport type { ProxyHost } from \"./models\";\n\nexport async function updateProxyHost(item: ProxyHost): Promise<ProxyHost> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/nginx/proxy-hosts/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateRedirectionHost.ts",
    "content": "import * as api from \"./base\";\nimport type { RedirectionHost } from \"./models\";\n\nexport async function updateRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/nginx/redirection-hosts/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateSetting.ts",
    "content": "import * as api from \"./base\";\nimport type { Setting } from \"./models\";\n\nexport async function updateSetting(item: Setting): Promise<Setting> {\n\t// Remove readonly fields\n\tconst { id, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/settings/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateStream.ts",
    "content": "import * as api from \"./base\";\nimport type { Stream } from \"./models\";\n\nexport async function updateStream(item: Stream): Promise<Stream> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/nginx/streams/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/updateUser.ts",
    "content": "import * as api from \"./base\";\nimport type { User } from \"./models\";\n\nexport async function updateUser(item: User): Promise<User> {\n\t// Remove readonly fields\n\tconst { id, createdOn: _, modifiedOn: __, ...data } = item;\n\n\treturn await api.put({\n\t\turl: `/users/${id}`,\n\t\tdata: data,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/uploadCertificate.ts",
    "content": "import * as api from \"./base\";\nimport type { Certificate } from \"./models\";\n\nexport async function uploadCertificate(id: number, data: FormData): Promise<Certificate> {\n\treturn await api.post({\n\t\turl: `/nginx/certificates/${id}/upload`,\n\t\tdata,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/api/backend/validateCertificate.ts",
    "content": "import * as api from \"./base\";\nimport type { ValidatedCertificateResponse } from \"./responseTypes\";\n\nexport async function validateCertificate(data: FormData): Promise<ValidatedCertificateResponse> {\n\treturn await api.post({\n\t\turl: \"/nginx/certificates/validate\",\n\t\tdata,\n\t});\n}\n"
  },
  {
    "path": "frontend/src/components/Button.tsx",
    "content": "import cn from \"classnames\";\nimport type { ReactNode } from \"react\";\n\ninterface Props {\n\tchildren: ReactNode;\n\tclassName?: string;\n\ttype?: \"button\" | \"submit\";\n\tactionType?: \"primary\" | \"secondary\" | \"success\" | \"warning\" | \"danger\" | \"info\" | \"light\" | \"dark\";\n\tvariant?: \"ghost\" | \"outline\" | \"pill\" | \"square\" | \"action\";\n\tsize?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n\tfullWidth?: boolean;\n\tisLoading?: boolean;\n\tdisabled?: boolean;\n\tcolor?:\n\t\t| \"blue\"\n\t\t| \"azure\"\n\t\t| \"indigo\"\n\t\t| \"purple\"\n\t\t| \"pink\"\n\t\t| \"red\"\n\t\t| \"orange\"\n\t\t| \"yellow\"\n\t\t| \"lime\"\n\t\t| \"green\"\n\t\t| \"teal\"\n\t\t| \"cyan\";\n\tonClick?: () => void;\n}\nfunction Button({\n\tchildren,\n\tclassName,\n\tonClick,\n\ttype,\n\tactionType,\n\tvariant,\n\tsize,\n\tcolor,\n\tfullWidth,\n\tisLoading,\n\tdisabled,\n}: Props) {\n\tconst myOnClick = () => {\n\t\t!isLoading && onClick && onClick();\n\t};\n\n\tconst cns = cn(\n\t\t\"btn\",\n\t\tclassName,\n\t\tactionType && `btn-${actionType}`,\n\t\tvariant && `btn-${variant}`,\n\t\tsize && `btn-${size}`,\n\t\tcolor && `btn-${color}`,\n\t\tfullWidth && \"w-100\",\n\t\tisLoading && \"btn-loading\",\n\t);\n\n\treturn (\n\t\t<button type={type || \"button\"} className={cns} onClick={myOnClick} disabled={disabled}>\n\t\t\t{children}\n\t\t</button>\n\t);\n}\n\nexport { Button };\n"
  },
  {
    "path": "frontend/src/components/EmptyData.tsx",
    "content": "import type { Table as ReactTable } from \"@tanstack/react-table\";\nimport cn from \"classnames\";\nimport type { ReactNode } from \"react\";\nimport { Button, HasPermission } from \"src/components\";\nimport { T } from \"src/locale\";\nimport { type ADMIN, MANAGE, type Permission, type Section } from \"src/modules/Permissions\";\n\ninterface Props {\n\ttableInstance: ReactTable<any>;\n\tonNew?: () => void;\n\tisFiltered?: boolean;\n\tobject: string;\n\tobjects: string;\n\tcolor?: string;\n\tcustomAddBtn?: ReactNode;\n\tpermissionSection?: Section | typeof ADMIN;\n\tpermission?: Permission;\n}\nfunction EmptyData({\n\ttableInstance,\n\tonNew,\n\tisFiltered,\n\tobject,\n\tobjects,\n\tcolor = \"primary\",\n\tcustomAddBtn,\n\tpermissionSection,\n\tpermission,\n}: Props) {\n\treturn (\n\t\t<tr>\n\t\t\t<td colSpan={tableInstance.getVisibleFlatColumns().length}>\n\t\t\t\t<div className=\"text-center my-4\">\n\t\t\t\t\t{isFiltered ? (\n\t\t\t\t\t\t<h2>\n\t\t\t\t\t\t\t<T id=\"empty-search\" />\n\t\t\t\t\t\t</h2>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<h2>\n\t\t\t\t\t\t\t\t<T id=\"object.empty\" tData={{ objects }} />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<HasPermission section={permissionSection} permission={permission || MANAGE} hideError>\n\t\t\t\t\t\t\t\t<p className=\"text-muted\">\n\t\t\t\t\t\t\t\t\t<T id=\"empty-subtitle\" />\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t{customAddBtn ? (\n\t\t\t\t\t\t\t\t\tcustomAddBtn\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<Button className={cn(\"my-3\", `btn-${color}`)} onClick={onNew}>\n\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object }} />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</td>\n\t\t</tr>\n\t);\n}\n\nexport { EmptyData };\n"
  },
  {
    "path": "frontend/src/components/ErrorNotFound.tsx",
    "content": "import { useNavigate } from \"react-router-dom\";\nimport { Button } from \"src/components\";\nimport { T } from \"src/locale\";\n\nexport function ErrorNotFound() {\n\tconst navigate = useNavigate();\n\n\treturn (\n\t\t<div className=\"container-tight py-4\">\n\t\t\t<div className=\"empty\">\n\t\t\t\t<p className=\"empty-title\">\n\t\t\t\t\t<T id=\"notfound.title\" />\n\t\t\t\t</p>\n\t\t\t\t<p className=\"empty-subtitle text-secondary\">\n\t\t\t\t\t<T id=\"notfound.content\" />\n\t\t\t\t</p>\n\t\t\t\t<div className=\"empty-action\">\n\t\t\t\t\t<Button type=\"button\" size=\"md\" onClick={() => navigate(\"/\")}>\n\t\t\t\t\t\t<T id=\"notfound.action\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Flag.tsx",
    "content": "import { IconWorld } from \"@tabler/icons-react\";\nimport { hasFlag } from \"country-flag-icons\";\n// @ts-expect-error Creating a typing for a subfolder is not easily possible\nimport Flags from \"country-flag-icons/react/3x2\";\n\ninterface FlagProps {\n\tclassName?: string;\n\tcountryCode: string;\n}\nfunction Flag({ className, countryCode }: FlagProps) {\n\tcountryCode = countryCode.toUpperCase();\n\tif (countryCode === \"EN\") {\n\t\treturn <IconWorld className={className} width={20} />;\n\t}\n\n\tif (hasFlag(countryCode)) {\n\t\tconst FlagElement = Flags[countryCode] as any;\n\t\treturn <FlagElement title={countryCode} className={className} width={20} />;\n\t}\n\tconsole.error(`No flag for country ${countryCode} found!`);\n\treturn null;\n}\n\nexport { Flag };\n"
  },
  {
    "path": "frontend/src/components/Form/AccessClientFields.tsx",
    "content": "import { IconX } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport { useFormikContext } from \"formik\";\nimport { useState } from \"react\";\nimport type { AccessListClient } from \"src/api/backend\";\nimport { intl, T } from \"src/locale\";\n\ninterface Props {\n\tinitialValues: AccessListClient[];\n\tname?: string;\n}\nexport function AccessClientFields({ initialValues, name = \"clients\" }: Props) {\n\tconst [values, setValues] = useState<AccessListClient[]>(initialValues || []);\n\tconst { setFieldValue } = useFormikContext();\n\n\tconst blankClient: AccessListClient = { directive: \"allow\", address: \"\" };\n\n\tif (values?.length === 0) {\n\t\tsetValues([blankClient]);\n\t}\n\n\tconst handleAdd = () => {\n\t\tsetValues([...values, blankClient]);\n\t};\n\n\tconst handleRemove = (idx: number) => {\n\t\tconst newValues = values.filter((_: AccessListClient, i: number) => i !== idx);\n\t\tif (newValues.length === 0) {\n\t\t\tnewValues.push(blankClient);\n\t\t}\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst handleChange = (idx: number, field: string, fieldValue: string) => {\n\t\tconst newValues = values.map((v: AccessListClient, i: number) =>\n\t\t\ti === idx ? { ...v, [field]: fieldValue } : v,\n\t\t);\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst setFormField = (newValues: AccessListClient[]) => {\n\t\tconst filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== \"\");\n\t\tsetFieldValue(name, filtered);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<p className=\"text-muted\">\n\t\t\t\t<T id=\"access-list.help.rules-order\" />\n\t\t\t</p>\n\t\t\t{values.map((client: AccessListClient, idx: number) => (\n\t\t\t\t<div className=\"row mb-1\" key={idx}>\n\t\t\t\t\t<div className=\"col-11\">\n\t\t\t\t\t\t<div className=\"input-group mb-2\">\n\t\t\t\t\t\t\t<span className=\"input-group-select\">\n\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\"form-select\",\n\t\t\t\t\t\t\t\t\t\t\"m-0\",\n\t\t\t\t\t\t\t\t\t\tclient.directive === \"allow\" ? \"bg-lime-lt\" : \"bg-orange-lt\",\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\tname={`clients[${idx}].directive`}\n\t\t\t\t\t\t\t\t\tvalue={client.directive}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"directive\", e.target.value)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<option value=\"allow\"><T id=\"action.allow\" /></option>\n\t\t\t\t\t\t\t\t\t<option value=\"deny\"><T id=\"action.deny\" /></option>\n\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tname={`clients[${idx}].address`}\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\tvalue={client.address}\n\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"address\", e.target.value)}\n\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"access-list.rule-source.placeholder\" })}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"col-1\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\tclassName=\"btn btn-ghost btn-danger p-0\"\n\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\thandleRemove(idx);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<IconX size={16} />\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t<div className=\"mb-3\">\n\t\t\t\t<button type=\"button\" className=\"btn btn-sm\" onClick={handleAdd}>\n\t\t\t\t\t<T id=\"action.add\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div className=\"row mb-3\">\n\t\t\t\t<p className=\"text-muted\">\n\t\t\t\t\t<T id=\"access-list.help-rules-last\" />\n\t\t\t\t</p>\n\t\t\t\t<div className=\"col-11\">\n\t\t\t\t\t<div className=\"input-group mb-2\">\n\t\t\t\t\t\t<span className=\"input-group-select\">\n\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\tclassName=\"form-select m-0 bg-orange-lt\"\n\t\t\t\t\t\t\t\tname=\"clients[last].directive\"\n\t\t\t\t\t\t\t\tvalue=\"deny\"\n\t\t\t\t\t\t\t\tdisabled\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<option value=\"deny\"><T id=\"action.deny\" /></option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tname=\"clients[last].address\"\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\tvalue=\"all\"\n\t\t\t\t\t\t\tdisabled\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/AccessField.tsx",
    "content": "import { IconLock, IconLockOpen2 } from \"@tabler/icons-react\";\nimport { Field, useFormikContext } from \"formik\";\nimport type { ReactNode } from \"react\";\nimport Select, { type ActionMeta, components, type OptionProps } from \"react-select\";\nimport type { AccessList } from \"src/api/backend\";\nimport { useLocaleState } from \"src/context\";\nimport { useAccessLists } from \"src/hooks\";\nimport { formatDateTime, intl, T } from \"src/locale\";\n\ninterface AccessOption {\n\treadonly value: number;\n\treadonly label: string;\n\treadonly subLabel: string;\n\treadonly icon: ReactNode;\n}\n\nconst Option = (props: OptionProps<AccessOption>) => {\n\treturn (\n\t\t<components.Option {...props}>\n\t\t\t<div className=\"flex-fill\">\n\t\t\t\t<div className=\"font-weight-medium\">\n\t\t\t\t\t{props.data.icon} <strong>{props.data.label}</strong>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"text-secondary mt-1 ps-3\">{props.data.subLabel}</div>\n\t\t\t</div>\n\t\t</components.Option>\n\t);\n};\n\ninterface Props {\n\tid?: string;\n\tname?: string;\n\tlabel?: string;\n}\nexport function AccessField({ name = \"accessListId\", label = \"access-list\", id = \"accessListId\" }: Props) {\n\tconst { locale } = useLocaleState();\n\tconst { isLoading, isError, error, data } = useAccessLists([\"owner\", \"items\", \"clients\"]);\n\tconst { setFieldValue } = useFormikContext();\n\n\tconst handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {\n\t\tsetFieldValue(name, newValue?.value);\n\t};\n\n\tconst options: AccessOption[] =\n\t\tdata?.map((item: AccessList) => ({\n\t\t\tvalue: item.id || 0,\n\t\t\tlabel: item.name,\n\t\t\tsubLabel: intl.formatMessage(\n\t\t\t\t{ id: \"access-list.subtitle\" },\n\t\t\t\t{\n\t\t\t\t\tusers: item?.items?.length,\n\t\t\t\t\trules: item?.clients?.length,\n\t\t\t\t\tdate: item?.createdOn ? formatDateTime(item?.createdOn, locale) : \"N/A\",\n\t\t\t\t},\n\t\t\t),\n\t\t\ticon: <IconLock size={14} className=\"text-lime\" />,\n\t\t})) || [];\n\n\t// Public option\n\toptions?.unshift({\n\t\tvalue: 0,\n\t\tlabel: intl.formatMessage({ id: \"access-list.public\" }),\n\t\tsubLabel: intl.formatMessage({ id: \"access-list.public.subtitle\" }),\n\t\ticon: <IconLockOpen2 size={14} className=\"text-red\" />,\n\t});\n\n\treturn (\n\t\t<Field name={name}>\n\t\t\t{({ field, form }: any) => (\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<label className=\"form-label\" htmlFor={id}>\n\t\t\t\t\t\t<T id={label} />\n\t\t\t\t\t</label>\n\t\t\t\t\t{isLoading ? <div className=\"placeholder placeholder-lg col-12 my-3 placeholder-glow\" /> : null}\n\t\t\t\t\t{isError ? <div className=\"invalid-feedback\">{`${error}`}</div> : null}\n\t\t\t\t\t{!isLoading && !isError ? (\n\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\tclassName=\"react-select-container\"\n\t\t\t\t\t\t\tclassNamePrefix=\"react-select\"\n\t\t\t\t\t\t\tdefaultValue={options.find((o) => o.value === field.value) || options[0]}\n\t\t\t\t\t\t\toptions={options}\n\t\t\t\t\t\t\tcomponents={{ Option }}\n\t\t\t\t\t\t\tstyles={{\n\t\t\t\t\t\t\t\toption: (base) => ({\n\t\t\t\t\t\t\t\t\t...base,\n\t\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : null}\n\t\t\t\t\t{form.errors[field.name] ? (\n\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</Field>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/BasicAuthFields.tsx",
    "content": "import { IconX } from \"@tabler/icons-react\";\nimport { useFormikContext } from \"formik\";\nimport { useState } from \"react\";\nimport type { AccessListItem } from \"src/api/backend\";\nimport { T } from \"src/locale\";\n\ninterface Props {\n\tinitialValues: AccessListItem[];\n\tname?: string;\n}\nexport function BasicAuthFields({ initialValues, name = \"items\" }: Props) {\n\tconst [values, setValues] = useState<AccessListItem[]>(initialValues || []);\n\tconst { setFieldValue } = useFormikContext();\n\n\tconst blankItem: AccessListItem = { username: \"\", password: \"\" };\n\n\tif (values?.length === 0) {\n\t\tsetValues([blankItem]);\n\t}\n\n\tconst handleAdd = () => {\n\t\tsetValues([...values, blankItem]);\n\t};\n\n\tconst handleRemove = (idx: number) => {\n\t\tconst newValues = values.filter((_: AccessListItem, i: number) => i !== idx);\n\t\tif (newValues.length === 0) {\n\t\t\tnewValues.push(blankItem);\n\t\t}\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst handleChange = (idx: number, field: string, fieldValue: string) => {\n\t\tconst newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst setFormField = (newValues: AccessListItem[]) => {\n\t\tconst filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== \"\");\n\t\tsetFieldValue(name, filtered);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"row\">\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<label className=\"form-label\" htmlFor=\"...\">\n\t\t\t\t\t\t<T id=\"username\" />\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<label className=\"form-label\" htmlFor=\"...\">\n\t\t\t\t\t\t<T id=\"password\" />\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t{values.map((item: AccessListItem, idx: number) => (\n\t\t\t\t<div className=\"row mb-3\" key={idx}>\n\t\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\tclassName=\"form-control input-sm\"\n\t\t\t\t\t\t\tvalue={item.username}\n\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"username\", e.target.value)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"col-5\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\tvalue={item.password}\n\t\t\t\t\t\t\tplaceholder={\n\t\t\t\t\t\t\t\tinitialValues.filter((iv: AccessListItem) => iv.username === item.username).length > 0\n\t\t\t\t\t\t\t\t\t? \"••••••••\"\n\t\t\t\t\t\t\t\t\t: \"\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"password\", e.target.value)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"col-1\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\tclassName=\"btn btn-ghost btn-danger p-0\"\n\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\thandleRemove(idx);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<IconX size={16} />\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t<div>\n\t\t\t\t<button type=\"button\" className=\"btn btn-sm\" onClick={handleAdd}>\n\t\t\t\t\t<T id=\"action.add\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/DNSProviderFields.module.css",
    "content": ".dnsChallengeWarning {\n\tborder: 1px solid var(--tblr-orange-lt);\n\tpadding: 1rem;\n\tborder-radius: 0.375rem;\n\tmargin-top: 1rem;\n\tbackground-color: var(--tblr-cyan-lt);\n}\n\n"
  },
  {
    "path": "frontend/src/components/Form/DNSProviderFields.tsx",
    "content": "import { IconAlertTriangle } from \"@tabler/icons-react\";\nimport CodeEditor from \"@uiw/react-textarea-code-editor\";\nimport { Field, useFormikContext } from \"formik\";\nimport { useState } from \"react\";\nimport Select, { type ActionMeta } from \"react-select\";\nimport type { DNSProvider } from \"src/api/backend\";\nimport { useDnsProviders } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport styles from \"./DNSProviderFields.module.css\";\n\ninterface DNSProviderOption {\n\treadonly value: string;\n\treadonly label: string;\n\treadonly credentials: string;\n}\n\ninterface Props {\n\tshowBoundaryBox?: boolean;\n}\nexport function DNSProviderFields({ showBoundaryBox = false }: Props) {\n\tconst { values, setFieldValue } = useFormikContext();\n\tconst { data: dnsProviders, isLoading } = useDnsProviders();\n\tconst [dnsProviderId, setDnsProviderId] = useState<string | null>(null);\n\n\tconst v: any = values || {};\n\n\tconst handleChange = (newValue: any, _actionMeta: ActionMeta<DNSProviderOption>) => {\n\t\tsetFieldValue(\"meta.dnsProvider\", newValue?.value);\n\t\tsetFieldValue(\"meta.dnsProviderCredentials\", newValue?.credentials);\n\t\tsetDnsProviderId(newValue?.value);\n\t};\n\n\tconst options: DNSProviderOption[] =\n\t\tdnsProviders?.map((p: DNSProvider) => ({\n\t\t\tvalue: p.id,\n\t\t\tlabel: p.name,\n\t\t\tcredentials: p.credentials,\n\t\t})) || [];\n\n\treturn (\n\t\t<div className={showBoundaryBox ? styles.dnsChallengeWarning : undefined}>\n\t\t\t<p className=\"text-warning\">\n\t\t\t\t<IconAlertTriangle size={16} className=\"me-1\" />\n\t\t\t\t<T id=\"certificates.dns.warning\" />\n\t\t\t</p>\n\n\t\t\t<Field name=\"meta.dnsProvider\">\n\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t<label htmlFor=\"dnsProvider\" className=\"form-label\">\n\t\t\t\t\t\t\t<T id=\"certificates.dns.provider\" />\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\tclassName=\"react-select-container\"\n\t\t\t\t\t\t\tclassNamePrefix=\"react-select\"\n\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\tid=\"dnsProvider\"\n\t\t\t\t\t\t\tcloseMenuOnSelect={true}\n\t\t\t\t\t\t\tisClearable={false}\n\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"certificates.dns.provider.placeholder\" })}\n\t\t\t\t\t\t\tisLoading={isLoading}\n\t\t\t\t\t\t\tisSearchable\n\t\t\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\t\t\toptions={options}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</Field>\n\n\t\t\t{dnsProviderId ? (\n\t\t\t\t<>\n\t\t\t\t\t<Field name=\"meta.dnsProviderCredentials\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<div className=\"mt-3\">\n\t\t\t\t\t\t\t\t<label htmlFor=\"dnsProviderCredentials\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"certificates.dns.credentials\" />\n\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\t\t\tlanguage=\"bash\"\n\t\t\t\t\t\t\t\t\tid=\"dnsProviderCredentials\"\n\t\t\t\t\t\t\t\t\tpadding={15}\n\t\t\t\t\t\t\t\t\tdata-color-mode=\"dark\"\n\t\t\t\t\t\t\t\t\tminHeight={130}\n\t\t\t\t\t\t\t\t\tindentWidth={2}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tfontFamily:\n\t\t\t\t\t\t\t\t\t\t\t\"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace\",\n\t\t\t\t\t\t\t\t\t\tborderRadius: \"0.3rem\",\n\t\t\t\t\t\t\t\t\t\tminHeight: \"130px\",\n\t\t\t\t\t\t\t\t\t\tbackgroundColor: \"var(--tblr-bg-surface-dark)\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tvalue={v.meta.dnsProviderCredentials || \"\"}\n\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t<small className=\"text-muted\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.dns.credentials-note\" />\n\t\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t<small className=\"text-danger\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.dns.credentials-warning\" />\n\t\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t\t<Field name=\"meta.propagationSeconds\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<div className=\"mt-3\">\n\t\t\t\t\t\t\t\t<label htmlFor=\"propagationSeconds\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"certificates.dns.propagation-seconds\" />\n\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tid=\"propagationSeconds\"\n\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\tmin={0}\n\t\t\t\t\t\t\t\t\tmax={7200}\n\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<small className=\"text-muted\">\n\t\t\t\t\t\t\t\t\t<T id=\"certificates.dns.propagation-seconds-note\" />\n\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t</>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/DomainNamesField.tsx",
    "content": "import { Field, useFormikContext } from \"formik\";\nimport type { ReactNode } from \"react\";\nimport type { ActionMeta, MultiValue } from \"react-select\";\nimport CreatableSelect from \"react-select/creatable\";\nimport { intl, T } from \"src/locale\";\nimport { validateDomain, validateDomains } from \"src/modules/Validations\";\n\ntype SelectOption = {\n\tlabel: string;\n\tvalue: string;\n\tcolor?: string;\n};\n\ninterface Props {\n\tid?: string;\n\tmaxDomains?: number;\n\tisWildcardPermitted?: boolean;\n\tdnsProviderWildcardSupported?: boolean;\n\tname?: string;\n\tlabel?: string;\n\tonChange?: (domains: string[]) => void;\n}\nexport function DomainNamesField({\n\tname = \"domainNames\",\n\tlabel = \"domain-names\",\n\tid = \"domainNames\",\n\tmaxDomains,\n\tisWildcardPermitted = false,\n\tdnsProviderWildcardSupported = false,\n\tonChange,\n}: Props) {\n\tconst { setFieldValue } = useFormikContext();\n\n\tconst handleChange = (v: MultiValue<SelectOption>, _actionMeta: ActionMeta<SelectOption>) => {\n\t\tconst doms = v?.map((i: SelectOption) => {\n\t\t\treturn i.value;\n\t\t});\n\t\tsetFieldValue(name, doms);\n\t\tonChange?.(doms);\n\t};\n\n\tconst helperTexts: ReactNode[] = [];\n\tif (maxDomains) {\n\t\thelperTexts.push(<T id=\"domain-names.max\" data={{ count: maxDomains }} />);\n\t}\n\tif (!isWildcardPermitted) {\n\t\thelperTexts.push(<T id=\"domain-names.wildcards-not-permitted\" />);\n\t} else if (!dnsProviderWildcardSupported) {\n\t\thelperTexts.push(<T id=\"domain-names.wildcards-not-supported\" />);\n\t}\n\n\treturn (\n\t\t<Field name={name} validate={validateDomains(isWildcardPermitted && dnsProviderWildcardSupported, maxDomains)}>\n\t\t\t{({ field, form }: any) => (\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<label className=\"form-label\" htmlFor={id}>\n\t\t\t\t\t\t<T id={label} />\n\t\t\t\t\t</label>\n\t\t\t\t\t<CreatableSelect\n\t\t\t\t\t\tclassName=\"react-select-container\"\n\t\t\t\t\t\tclassNamePrefix=\"react-select\"\n\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\tid={id}\n\t\t\t\t\t\tcloseMenuOnSelect={true}\n\t\t\t\t\t\tisClearable={false}\n\t\t\t\t\t\tisValidNewOption={validateDomain(isWildcardPermitted && dnsProviderWildcardSupported)}\n\t\t\t\t\t\tisMulti\n\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"domain-names.placeholder\" })}\n\t\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\t\tvalue={field.value?.map((d: string) => ({ label: d, value: d }))}\n\t\t\t\t\t/>\n\t\t\t\t\t{form.errors[field.name] && form.touched[field.name] ? (\n\t\t\t\t\t\t<small className=\"text-danger\">{form.errors[field.name]}</small>\n\t\t\t\t\t) : helperTexts.length ? (\n\t\t\t\t\t\thelperTexts.map((i, idx) => (\n\t\t\t\t\t\t\t<small key={idx} className=\"text-info\">\n\t\t\t\t\t\t\t\t{i}\n\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t))\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</Field>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/LocationsFields.module.css",
    "content": ".locationCard {\n\tborder-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;\n}\n"
  },
  {
    "path": "frontend/src/components/Form/LocationsFields.tsx",
    "content": "import { IconSettings } from \"@tabler/icons-react\";\nimport CodeEditor from \"@uiw/react-textarea-code-editor\";\nimport cn from \"classnames\";\nimport { useFormikContext } from \"formik\";\nimport { useState } from \"react\";\nimport type { ProxyLocation } from \"src/api/backend\";\nimport { intl, T } from \"src/locale\";\nimport styles from \"./LocationsFields.module.css\";\n\ninterface Props {\n\tinitialValues: ProxyLocation[];\n\tname?: string;\n}\nexport function LocationsFields({ initialValues, name = \"locations\" }: Props) {\n\tconst [values, setValues] = useState<ProxyLocation[]>(initialValues || []);\n\tconst { setFieldValue } = useFormikContext();\n\tconst [advVisible, setAdvVisible] = useState<number[]>([]);\n\n\tconst blankItem: ProxyLocation = {\n\t\tpath: \"\",\n\t\tadvancedConfig: \"\",\n\t\tforwardScheme: \"http\",\n\t\tforwardHost: \"\",\n\t\tforwardPort: 80,\n\t};\n\n\tconst toggleAdvVisible = (idx: number) => {\n\t\tsetAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);\n\t};\n\n\tconst handleAdd = () => {\n\t\tsetValues([...values, blankItem]);\n\t};\n\n\tconst handleRemove = (idx: number) => {\n\t\tconst newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst handleChange = (idx: number, field: string, fieldValue: string) => {\n\t\tconst newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));\n\t\tsetValues(newValues);\n\t\tsetFormField(newValues);\n\t};\n\n\tconst setFormField = (newValues: ProxyLocation[]) => {\n\t\tconst filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== \"\");\n\t\tsetFieldValue(name, filtered);\n\t};\n\n\tif (values.length === 0) {\n\t\treturn (\n\t\t\t<div className=\"text-center\">\n\t\t\t\t<button type=\"button\" className=\"btn my-3\" onClick={handleAdd}>\n\t\t\t\t\t<T id=\"action.add-location\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t{values.map((item: ProxyLocation, idx: number) => (\n\t\t\t\t<div key={idx} className={cn(\"card\", \"card-active\", \"mb-3\", styles.locationCard)}>\n\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t<div className=\"col-md-10\">\n\t\t\t\t\t\t\t\t<div className=\"input-group mb-3\">\n\t\t\t\t\t\t\t\t\t<span className=\"input-group-text\">Location</span>\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"/path\"\n\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\tvalue={item.path}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"path\", e.target.value)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"col-md-2 text-end\">\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclassName=\"btn p-0\"\n\t\t\t\t\t\t\t\t\ttitle=\"Advanced\"\n\t\t\t\t\t\t\t\t\tonClick={() => toggleAdvVisible(idx)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconSettings size={20} />\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t<div className=\"col-md-3\">\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardScheme\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-scheme\" />\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\tid=\"forwardScheme\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\tvalue={item.forwardScheme}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"forwardScheme\", e.target.value)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<option value=\"http\">http</option>\n\t\t\t\t\t\t\t\t\t\t<option value=\"https\">https</option>\n\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"col-md-6\">\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardHost\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"proxy-host.forward-host\" />\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\tid=\"forwardHost\"\n\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"eg: 10.0.0.1/path/\"\n\t\t\t\t\t\t\t\t\t\tvalue={item.forwardHost}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"forwardHost\", e.target.value)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"col-md-3\">\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardPort\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-port\" />\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\tid=\"forwardPort\"\n\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\tmax={65535}\n\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"eg: 8081\"\n\t\t\t\t\t\t\t\t\t\tvalue={item.forwardPort}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"forwardPort\", e.target.value)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{advVisible.includes(idx) && (\n\t\t\t\t\t\t\t<div className=\"\">\n\t\t\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\t\t\tlanguage=\"nginx\"\n\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"nginx-config.placeholder\" })}\n\t\t\t\t\t\t\t\t\tpadding={15}\n\t\t\t\t\t\t\t\t\tdata-color-mode=\"dark\"\n\t\t\t\t\t\t\t\t\tminHeight={170}\n\t\t\t\t\t\t\t\t\tindentWidth={2}\n\t\t\t\t\t\t\t\t\tvalue={item.advancedConfig}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleChange(idx, \"advancedConfig\", e.target.value)}\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tfontFamily:\n\t\t\t\t\t\t\t\t\t\t\t\"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace\",\n\t\t\t\t\t\t\t\t\t\tborderRadius: \"0.3rem\",\n\t\t\t\t\t\t\t\t\t\tminHeight: \"170px\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div className=\"mt-1\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\thandleRemove(idx);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t<div>\n\t\t\t\t<button type=\"button\" className=\"btn btn-sm\" onClick={handleAdd}>\n\t\t\t\t\t<T id=\"action.add-location\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/NginxConfigField.tsx",
    "content": "import CodeEditor from \"@uiw/react-textarea-code-editor\";\nimport { Field } from \"formik\";\nimport { intl, T } from \"src/locale\";\n\ninterface Props {\n\tid?: string;\n\tname?: string;\n\tlabel?: string;\n}\nexport function NginxConfigField({\n\tname = \"advancedConfig\",\n\tlabel = \"nginx-config.label\",\n\tid = \"advancedConfig\",\n}: Props) {\n\treturn (\n\t\t<Field name={name}>\n\t\t\t{({ field }: any) => (\n\t\t\t\t<div className=\"mt-3\">\n\t\t\t\t\t<label htmlFor={id} className=\"form-label\">\n\t\t\t\t\t\t<T id={label} />\n\t\t\t\t\t</label>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tlanguage=\"nginx\"\n\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"nginx-config.placeholder\" })}\n\t\t\t\t\t\tpadding={15}\n\t\t\t\t\t\tdata-color-mode=\"dark\"\n\t\t\t\t\t\tminHeight={200}\n\t\t\t\t\t\tindentWidth={2}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tfontFamily: \"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace\",\n\t\t\t\t\t\t\tborderRadius: \"0.3rem\",\n\t\t\t\t\t\t\tminHeight: \"200px\",\n\t\t\t\t\t\t\tbackgroundColor: \"var(--tblr-bg-surface-dark)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\t{...field}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</Field>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/SSLCertificateField.tsx",
    "content": "import { IconShield } from \"@tabler/icons-react\";\nimport { Field, useFormikContext } from \"formik\";\nimport Select, { type ActionMeta, components, type OptionProps } from \"react-select\";\nimport type { Certificate } from \"src/api/backend\";\nimport { useLocaleState } from \"src/context\";\nimport { useCertificates } from \"src/hooks\";\nimport { formatDateTime, intl, T } from \"src/locale\";\n\ninterface CertOption {\n\treadonly value: number | \"new\";\n\treadonly label: string;\n\treadonly subLabel: string;\n\treadonly icon: React.ReactNode;\n}\n\nconst Option = (props: OptionProps<CertOption>) => {\n\treturn (\n\t\t<components.Option {...props}>\n\t\t\t<div className=\"flex-fill\">\n\t\t\t\t<div className=\"font-weight-medium\">\n\t\t\t\t\t{props.data.icon} <strong>{props.data.label}</strong>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"text-secondary mt-1 ps-3\">{props.data.subLabel}</div>\n\t\t\t</div>\n\t\t</components.Option>\n\t);\n};\n\ninterface Props {\n\tid?: string;\n\tname?: string;\n\tlabel?: string;\n\trequired?: boolean;\n\tallowNew?: boolean;\n\tforHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields\n}\nexport function SSLCertificateField({\n\tname = \"certificateId\",\n\tlabel = \"ssl-certificate\",\n\tid = \"certificateId\",\n\trequired,\n\tallowNew,\n\tforHttp = true,\n}: Props) {\n\tconst { locale } = useLocaleState();\n\tconst { isLoading, isError, error, data } = useCertificates();\n\tconst { values, setFieldValue } = useFormikContext();\n\tconst v: any = values || {};\n\n\tconst handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => {\n\t\tsetFieldValue(name, newValue?.value);\n\t\tconst {\n\t\t\tsslForced,\n\t\t\thttp2Support,\n\t\t\thstsEnabled,\n\t\t\thstsSubdomains,\n\t\t\tdnsChallenge,\n\t\t\tdnsProvider,\n\t\t\tdnsProviderCredentials,\n\t\t\tpropagationSeconds,\n\t\t} = v;\n\t\tif (forHttp && !newValue?.value) {\n\t\t\tsslForced && setFieldValue(\"sslForced\", false);\n\t\t\thttp2Support && setFieldValue(\"http2Support\", false);\n\t\t\thstsEnabled && setFieldValue(\"hstsEnabled\", false);\n\t\t\thstsSubdomains && setFieldValue(\"hstsSubdomains\", false);\n\t\t}\n\t\tif (newValue?.value !== \"new\") {\n\t\t\tdnsChallenge && setFieldValue(\"dnsChallenge\", undefined);\n\t\t\tdnsProvider && setFieldValue(\"dnsProvider\", undefined);\n\t\t\tdnsProviderCredentials && setFieldValue(\"dnsProviderCredentials\", undefined);\n\t\t\tpropagationSeconds && setFieldValue(\"propagationSeconds\", undefined);\n\t\t}\n\t};\n\n\tconst options: CertOption[] =\n\t\tdata?.map((cert: Certificate) => ({\n\t\t\tvalue: cert.id,\n\t\t\tlabel: cert.niceName,\n\t\t\tsubLabel: `${cert.provider === \"letsencrypt\" ? intl.formatMessage({ id: \"lets-encrypt\" }) : cert.provider} — ${intl.formatMessage({ id: \"expires.on\" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn, locale) : \"N/A\" })}`,\n\t\t\ticon: <IconShield size={14} className=\"text-pink\" />,\n\t\t})) || [];\n\n\t// Prepend the Add New option\n\tif (allowNew) {\n\t\toptions?.unshift({\n\t\t\tvalue: \"new\",\n\t\t\tlabel: intl.formatMessage({ id: \"certificates.request.title\" }),\n\t\t\tsubLabel: intl.formatMessage({ id: \"certificates.request.subtitle\" }),\n\t\t\ticon: <IconShield size={14} className=\"text-lime\" />,\n\t\t});\n\t}\n\n\t// Prepend the None option\n\tif (!required) {\n\t\toptions?.unshift({\n\t\t\tvalue: 0,\n\t\t\tlabel: intl.formatMessage({ id: \"certificate.none.title\" }),\n\t\t\tsubLabel: forHttp\n\t\t\t\t? intl.formatMessage({ id: \"certificate.none.subtitle.for-http\" })\n\t\t\t\t: intl.formatMessage({ id: \"certificate.none.subtitle\" }),\n\t\t\ticon: <IconShield size={14} className=\"text-red\" />,\n\t\t});\n\t}\n\n\treturn (\n\t\t<Field name={name}>\n\t\t\t{({ field, form }: any) => (\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<label className=\"form-label\" htmlFor={id}>\n\t\t\t\t\t\t<T id={label} />\n\t\t\t\t\t</label>\n\t\t\t\t\t{isLoading ? <div className=\"placeholder placeholder-lg col-12 my-3 placeholder-glow\" /> : null}\n\t\t\t\t\t{isError ? <div className=\"invalid-feedback\">{`${error}`}</div> : null}\n\t\t\t\t\t{!isLoading && !isError ? (\n\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\tclassName=\"react-select-container\"\n\t\t\t\t\t\t\tclassNamePrefix=\"react-select\"\n\t\t\t\t\t\t\tdefaultValue={options.find((o) => o.value === field.value) || options[0]}\n\t\t\t\t\t\t\toptions={options}\n\t\t\t\t\t\t\tcomponents={{ Option }}\n\t\t\t\t\t\t\tstyles={{\n\t\t\t\t\t\t\t\toption: (base) => ({\n\t\t\t\t\t\t\t\t\t...base,\n\t\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : null}\n\t\t\t\t\t{form.errors[field.name] ? (\n\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</Field>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/SSLOptionsFields.tsx",
    "content": "import cn from \"classnames\";\nimport { Field, useFormikContext } from \"formik\";\nimport { DNSProviderFields, DomainNamesField } from \"src/components\";\nimport { T } from \"src/locale\";\n\ninterface Props {\n\tforHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields\n\tforProxyHost?: boolean; // the advanced fields\n\tforceDNSForNew?: boolean;\n\trequireDomainNames?: boolean; // used for streams\n\tcolor?: string;\n}\nexport function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDNSForNew, requireDomainNames, color = \"bg-cyan\" }: Props) {\n\tconst { values, setFieldValue } = useFormikContext();\n\tconst v: any = values || {};\n\n\tconst newCertificate = v?.certificateId === \"new\";\n\tconst hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);\n\tconst { sslForced, http2Support, hstsEnabled, hstsSubdomains, trustForwardedProto, meta } = v;\n\tconst { dnsChallenge } = meta || {};\n\n\tif (forceDNSForNew && newCertificate && !dnsChallenge) {\n\t\tsetFieldValue(\"meta.dnsChallenge\", true);\n\t}\n\n\tconst handleToggleChange = (e: any, fieldName: string) => {\n\t\tsetFieldValue(fieldName, e.target.checked);\n\t\tif (fieldName === \"meta.dnsChallenge\" && !e.target.checked) {\n\t\t\tsetFieldValue(\"meta.dnsProvider\", undefined);\n\t\t\tsetFieldValue(\"meta.dnsProviderCredentials\", undefined);\n\t\t\tsetFieldValue(\"meta.propagationSeconds\", undefined);\n\t\t}\n\t};\n\n\tconst toggleClasses = \"form-check-input\";\n\tconst toggleEnabled = cn(toggleClasses, color);\n\n\tconst getHttpOptions = () => (\n\t\t<div>\n\t\t\t<div className=\"row\">\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<Field name=\"sslForced\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclassName={sslForced ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\tchecked={!!sslForced}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t\tdisabled={!hasCertificate}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"domains.force-ssl\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<Field name=\"http2Support\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclassName={http2Support ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\tchecked={!!http2Support}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t\tdisabled={!hasCertificate}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"domains.http2-support\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div className=\"row\">\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<Field name=\"hstsEnabled\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclassName={hstsEnabled ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\tchecked={!!hstsEnabled}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t\tdisabled={!hasCertificate || !sslForced}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"domains.hsts-enabled\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"col-6\">\n\t\t\t\t\t<Field name=\"hstsSubdomains\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclassName={hstsSubdomains ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\tchecked={!!hstsSubdomains}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t\tdisabled={!hasCertificate || !hstsEnabled}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"domains.hsts-subdomains\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n\n\tconst getHttpAdvancedOptions = () =>(\n\t\t<div>\n\t\t\t<details>\n\t\t\t\t<summary className=\"mb-1\"><T id=\"domains.advanced\" /></summary>\n\t\t\t\t<div className=\"row\">\n\t\t\t\t\t<div className=\"col-12\">\n\t\t\t\t\t\t<Field name=\"trustForwardedProto\">\n\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\tclassName={trustForwardedProto ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\tchecked={!!trustForwardedProto}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t\t\tdisabled={!hasCertificate || !sslForced}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"domains.trust-forwarded-proto\" />\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Field>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</details>\n\t\t</div>\n\t);\n\n\treturn (\n\t\t<div>\n\t\t\t{forHttp ? getHttpOptions() : null}\n\t\t\t{newCertificate ? (\n\t\t\t\t<>\n\t\t\t\t\t<Field name=\"meta.dnsChallenge\">\n\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t<label className=\"form-check form-switch mt-1\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclassName={dnsChallenge ? toggleEnabled : toggleClasses}\n\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\tchecked={forceDNSForNew ? true : !!dnsChallenge}\n\t\t\t\t\t\t\t\t\tdisabled={forceDNSForNew}\n\t\t\t\t\t\t\t\t\tonChange={(e) => handleToggleChange(e, field.name)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<span className=\"form-check-label\">\n\t\t\t\t\t\t\t\t\t<T id=\"domains.use-dns\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Field>\n\t\t\t\t\t{requireDomainNames ? <DomainNamesField isWildcardPermitted dnsProviderWildcardSupported /> : null}\n\t\t\t\t\t{dnsChallenge ? <DNSProviderFields showBoundaryBox /> : null}\n\t\t\t\t</>\n\t\t\t) : null}\n\t\t\t{forProxyHost && forHttp ? getHttpAdvancedOptions() : null}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Form/index.ts",
    "content": "export * from \"./AccessClientFields\";\nexport * from \"./AccessField\";\nexport * from \"./BasicAuthFields\";\nexport * from \"./DNSProviderFields\";\nexport * from \"./DomainNamesField\";\nexport * from \"./LocationsFields\";\nexport * from \"./NginxConfigField\";\nexport * from \"./SSLCertificateField\";\nexport * from \"./SSLOptionsFields\";\n"
  },
  {
    "path": "frontend/src/components/HasPermission.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { Loading, LoadingPage } from \"src/components\";\nimport { useUser } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { type ADMIN, hasPermission, type Permission, type Section } from \"src/modules/Permissions\";\n\ninterface Props {\n\tsection?: Section | typeof ADMIN;\n\tpermission: Permission;\n\thideError?: boolean;\n\tchildren?: ReactNode;\n\tpageLoading?: boolean;\n\tloadingNoLogo?: boolean;\n}\nfunction HasPermission({\n\tsection,\n\tpermission,\n\tchildren,\n\thideError = false,\n\tpageLoading = false,\n\tloadingNoLogo = false,\n}: Props) {\n\tconst { data, isLoading } = useUser(\"me\");\n\n\tif (!section) {\n\t\treturn <>{children}</>;\n\t}\n\n\tif (isLoading) {\n\t\tif (hideError) {\n\t\t\treturn null;\n\t\t}\n\t\tif (pageLoading) {\n\t\t\treturn <LoadingPage noLogo={loadingNoLogo} />;\n\t\t}\n\t\treturn <Loading noLogo={loadingNoLogo} />;\n\t}\n\n\tconst allowed = hasPermission(section, permission, data?.permissions, data?.roles);\n\tif (allowed) {\n\t\treturn <>{children}</>;\n\t}\n\n\treturn !hideError ? (\n\t\t<Alert variant=\"danger\">\n\t\t\t<T id=\"no-permission-error\" />\n\t\t</Alert>\n\t) : null;\n}\n\nexport { HasPermission };\n"
  },
  {
    "path": "frontend/src/components/Loading.module.css",
    "content": ".logo {\n\tmax-height: 100px;\n}\n"
  },
  {
    "path": "frontend/src/components/Loading.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { T } from \"src/locale\";\nimport styles from \"./Loading.module.css\";\n\ninterface Props {\n\tlabel?: string | ReactNode;\n\tnoLogo?: boolean;\n}\nexport function Loading({ label, noLogo }: Props) {\n\treturn (\n\t\t<div className=\"empty text-center\">\n\t\t\t{noLogo ? null : (\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<img className={styles.logo} src=\"/images/logo-no-text.svg\" alt=\"\" />\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t<div className=\"text-secondary mb-3\">{label || <T id=\"loading\" />}</div>\n\t\t\t<div className=\"progress progress-sm\">\n\t\t\t\t<div className=\"progress-bar progress-bar-indeterminate\" />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/LoadingPage.tsx",
    "content": "import { Loading, Page } from \"src/components\";\n\ninterface Props {\n\tlabel?: string;\n\tnoLogo?: boolean;\n}\nexport function LoadingPage({ label, noLogo }: Props) {\n\treturn (\n\t\t<Page className=\"page-center\">\n\t\t\t<div className=\"container-tight py-4\">\n\t\t\t\t<Loading label={label} noLogo={noLogo} />\n\t\t\t</div>\n\t\t</Page>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/LocalePicker.module.css",
    "content": ".btn {\n\tcolor: light-dark(var(--tblr-dark), var(--tblr-light)) !important;\n\n\t&:hover {\n\t\tborder: var(--tblr-btn-border-width) solid transparent !important;\n\t\tbackground: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;\n\t}\n}\n"
  },
  {
    "path": "frontend/src/components/LocalePicker.tsx",
    "content": "import cn from \"classnames\";\nimport { Flag } from \"src/components\";\nimport { useLocaleState } from \"src/context\";\nimport { useTheme } from \"src/hooks\";\nimport { changeLocale, getFlagCodeForLocale, localeOptions, T } from \"src/locale\";\nimport styles from \"./LocalePicker.module.css\";\n\ninterface Props {\n\tmenuAlign?: \"start\" | \"end\";\n}\n\nfunction LocalePicker({ menuAlign = \"start\" }: Props) {\n\tconst { locale, setLocale } = useLocaleState();\n\tconst { getTheme } = useTheme();\n\n\tconst changeTo = (lang: string) => {\n\t\tchangeLocale(lang);\n\t\tsetLocale(lang);\n\t\tlocation.reload();\n\t};\n\n\tconst classes = [\"btn\", \"dropdown-toggle\", \"btn-sm\", styles.btn];\n\tconst cns = cn(...classes, getTheme() === \"dark\" ? \"btn-ghost-dark\" : \"btn-ghost-light\");\n\n\treturn (\n\t\t<div className=\"dropdown\">\n\t\t\t<button type=\"button\" className={cns} data-bs-toggle=\"dropdown\">\n\t\t\t\t<Flag countryCode={getFlagCodeForLocale(locale)} />\n\t\t\t</button>\n\t\t\t<div\n\t\t\t\tclassName={cn(\"dropdown-menu\", {\n\t\t\t\t\t\"dropdown-menu-end\": menuAlign === \"end\",\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t{localeOptions.map((item: any) => (\n\t\t\t\t\t<a\n\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\thref={`/locale/${item[0]}`}\n\t\t\t\t\t\tkey={`locale-${item[0]}`}\n\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\tchangeTo(item[0]);\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />\n\t\t\t\t\t</a>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport { LocalePicker };\n"
  },
  {
    "path": "frontend/src/components/NavLink.tsx",
    "content": "import { useNavigate } from \"react-router-dom\";\n\ninterface Props {\n\tchildren: React.ReactNode;\n\tto?: string;\n\tisDropdownItem?: boolean;\n\tonClick?: () => void;\n}\nexport function NavLink({ children, to, isDropdownItem, onClick }: Props) {\n\tconst navigate = useNavigate();\n\n\treturn (\n\t\t<a\n\t\t\tclassName={isDropdownItem ? \"dropdown-item\" : \"nav-link\"}\n\t\t\thref={to}\n\t\t\tonClick={(e) => {\n\t\t\t\te.preventDefault();\n\t\t\t\tif (onClick) {\n\t\t\t\t\tonClick();\n\t\t\t\t}\n\t\t\t\tif (to) {\n\t\t\t\t\tnavigate(to);\n\t\t\t\t}\n\t\t\t}}\n\t\t>\n\t\t\t{children}\n\t\t</a>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Page.module.css",
    "content": ".page {\n\tdisplay: grid;\n\tgrid-template-rows: auto 1fr auto; /* Header, Main Content, Footer */\n\tmin-height: 100vh;\n}\n"
  },
  {
    "path": "frontend/src/components/Page.tsx",
    "content": "import cn from \"classnames\";\nimport styles from \"./Page.module.css\";\n\ninterface Props {\n\tchildren: React.ReactNode;\n\tclassName?: string;\n}\nexport function Page({ children, className }: Props) {\n\treturn <div className={cn(className, styles.page)}>{children}</div>;\n}\n"
  },
  {
    "path": "frontend/src/components/SiteContainer.tsx",
    "content": "interface Props {\n\tchildren: React.ReactNode;\n}\nexport function SiteContainer({ children }: Props) {\n\treturn <div className=\"container-xl py-3 min-w-0 overflow-x-auto\">{children}</div>;\n}\n"
  },
  {
    "path": "frontend/src/components/SiteFooter.tsx",
    "content": "import { useCheckVersion, useHealth } from \"src/hooks\";\nimport { T } from \"src/locale\";\n\nexport function SiteFooter() {\n\tconst health = useHealth();\n\tconst { data: versionData } = useCheckVersion();\n\n\tconst getVersion = () => {\n\t\tif (!health.data) {\n\t\t\treturn \"\";\n\t\t}\n\t\tconst v = health.data.version;\n\t\treturn `v${v.major}.${v.minor}.${v.revision}`;\n\t};\n\n\treturn (\n\t\t<footer className=\"footer d-print-none py-3\">\n\t\t\t<div className=\"container-xl\">\n\t\t\t\t<div className=\"row text-center align-items-center flex-row-reverse\">\n\t\t\t\t\t<div className=\"col-lg-auto ms-lg-auto\">\n\t\t\t\t\t\t<ul className=\"list-inline list-inline-dots mb-0\">\n\t\t\t\t\t\t\t<li className=\"list-inline-item\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"https://github.com/NginxProxyManager/nginx-proxy-manager\"\n\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\tclassName=\"link-secondary\"\n\t\t\t\t\t\t\t\t\trel=\"noopener\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"footer.github-fork\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"col-12 col-lg-auto mt-3 mt-lg-0\">\n\t\t\t\t\t\t<ul className=\"list-inline list-inline-dots mb-0\">\n\t\t\t\t\t\t\t<li className=\"list-inline-item\">\n\t\t\t\t\t\t\t\t© 2025{\" \"}\n\t\t\t\t\t\t\t\t<a href=\"https://jc21.com\" rel=\"noreferrer\" target=\"_blank\" className=\"link-secondary\">\n\t\t\t\t\t\t\t\t\tjc21.com\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t<li className=\"list-inline-item\">\n\t\t\t\t\t\t\t\tTheme by{\" \"}\n\t\t\t\t\t\t\t\t<a href=\"https://tabler.io\" rel=\"noreferrer\" target=\"_blank\" className=\"link-secondary\">\n\t\t\t\t\t\t\t\t\tTabler\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t<li className=\"list-inline-item\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`}\n\t\t\t\t\t\t\t\t\tclassName=\"link-secondary\"\n\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\trel=\"noopener\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t\t\t\t{getVersion()}{\" \"}\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t{versionData?.updateAvailable && versionData?.latest && (\n\t\t\t\t\t\t\t\t<li className=\"list-inline-item\">\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${versionData.latest}`}\n\t\t\t\t\t\t\t\t\t\tclassName=\"link-warning fw-bold\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\trel=\"noopener\"\n\t\t\t\t\t\t\t\t\t\ttitle={`New version ${versionData.latest} is available`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"update-available\" data={{ latestVersion: versionData.latest }} />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</footer>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/SiteHeader.module.css",
    "content": ".logo {\n\tfont-size: 1.1rem;\n\tfont-weight: 500;\n\n\timg {\n\t\tmargin-right: 0.8rem;\n\t}\n}\n"
  },
  {
    "path": "frontend/src/components/SiteHeader.tsx",
    "content": "import { IconLock, IconLogout, IconShieldLock, IconUser } from \"@tabler/icons-react\";\nimport { LocalePicker, NavLink, ThemeSwitcher } from \"src/components\";\nimport { useAuthState } from \"src/context\";\nimport { useUser } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showChangePasswordModal, showTwoFactorModal, showUserModal } from \"src/modals\";\nimport styles from \"./SiteHeader.module.css\";\n\nexport function SiteHeader() {\n\tconst { data: currentUser } = useUser(\"me\");\n\tconst isAdmin = currentUser?.roles.includes(\"admin\");\n\tconst { logout } = useAuthState();\n\n\treturn (\n\t\t<header className=\"navbar navbar-expand-md d-print-none\">\n\t\t\t<div className=\"container-xl\">\n\t\t\t\t<button\n\t\t\t\t\tclassName=\"navbar-toggler\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-bs-toggle=\"collapse\"\n\t\t\t\t\tdata-bs-target=\"#navbar-menu\"\n\t\t\t\t\taria-controls=\"navbar-menu\"\n\t\t\t\t\taria-expanded=\"false\"\n\t\t\t\t\taria-label=\"Toggle navigation\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"navbar-toggler-icon\" />\n\t\t\t\t</button>\n\t\t\t\t<div className=\"navbar-brand navbar-brand-autodark pe-0 pe-md-3\">\n\t\t\t\t\t<NavLink to=\"/\">\n\t\t\t\t\t\t<div className={styles.logo}>\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc=\"/images/logo-no-text.svg\"\n\t\t\t\t\t\t\t\twidth={40}\n\t\t\t\t\t\t\t\theight={40}\n\t\t\t\t\t\t\t\tclassName=\"navbar-brand-image\"\n\t\t\t\t\t\t\t\talt=\"Logo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\tNginx Proxy Manager\n\t\t\t\t\t</NavLink>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"navbar-nav flex-row order-md-last\">\n\t\t\t\t\t<div className=\"d-none d-md-flex\">\n\t\t\t\t\t\t<div className=\"nav-item\">\n\t\t\t\t\t\t\t<LocalePicker />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"nav-item\">\n\t\t\t\t\t\t\t<ThemeSwitcher />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"nav-item d-md-flex\">\n\t\t\t\t\t\t<div className=\"nav-item dropdown\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"/\"\n\t\t\t\t\t\t\t\tclassName=\"nav-link d-flex lh-1\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t\taria-label=\"Open user menu\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclassName=\"avatar avatar-sm\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tbackgroundImage: `url(${currentUser?.avatar || \"/images/default-avatar.jpg\"})`,\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<div className=\"d-none d-xl-block ps-2\">\n\t\t\t\t\t\t\t\t\t<div>{currentUser?.nickname}</div>\n\t\t\t\t\t\t\t\t\t<div className=\"mt-1 small text-secondary\">\n\t\t\t\t\t\t\t\t\t\t<T id={isAdmin ? \"role.admin\" : \"role.standard-user\"} />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end dropdown-menu-arrow\">\n\t\t\t\t\t\t\t\t<div className=\"d-md-none\">\n\t\t\t\t\t\t\t\t\t{/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: This div is not interactive. */}\n\t\t\t\t\t\t\t\t\t<div className=\"p-2 pb-1 pe-1 d-flex align-items-center\" onClick={e => e.stopPropagation()}>\n\t\t\t\t\t\t\t\t\t\t<div className=\"ps-2 pe-1 me-auto\">\n\t\t\t\t\t\t\t\t\t\t\t<div>{currentUser?.nickname}</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-1 small text-secondary text-nowrap\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id={isAdmin ? \"role.admin\" : \"role.standard-user\"} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"d-flex align-items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<ThemeSwitcher className=\"me-n2\" />\n\t\t\t\t\t\t\t\t\t\t\t<LocalePicker menuAlign=\"end\" />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"?\"\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tshowUserModal(\"me\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconUser width={18} />\n\t\t\t\t\t\t\t\t\t<T id=\"user.edit-profile\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"?\"\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tshowChangePasswordModal(\"me\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconLock width={18} />\n\t\t\t\t\t\t\t\t\t<T id=\"user.change-password\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"?\"\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tshowTwoFactorModal(\"me\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconShieldLock width={18} />\n\t\t\t\t\t\t\t\t\t<T id=\"user.two-factor\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"?\"\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tlogout();\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconLogout width={18} />\n\t\t\t\t\t\t\t\t\t<T id=\"user.logout\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</header>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/SiteMenu.tsx",
    "content": "import {\n\tIconBook,\n\tIconDeviceDesktop,\n\tIconHome,\n\tIconLock,\n\tIconSettings,\n\tIconShield,\n\tIconUser,\n} from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport React from \"react\";\nimport { HasPermission, NavLink } from \"src/components\";\nimport { T } from \"src/locale\";\nimport {\n\tACCESS_LISTS,\n\tADMIN,\n\tCERTIFICATES,\n\tDEAD_HOSTS,\n\ttype MANAGE,\n\tPROXY_HOSTS,\n\tREDIRECTION_HOSTS,\n\ttype Section,\n\tSTREAMS,\n\tVIEW,\n} from \"src/modules/Permissions\";\n\ninterface MenuItem {\n\tlabel: string;\n\ticon?: React.ElementType;\n\tto?: string;\n\titems?: MenuItem[];\n\tpermissionSection?: Section | typeof ADMIN;\n\tpermission?: typeof VIEW | typeof MANAGE;\n}\n\nconst menuItems: MenuItem[] = [\n\t{\n\t\tto: \"/\",\n\t\ticon: IconHome,\n\t\tlabel: \"dashboard\",\n\t},\n\t{\n\t\ticon: IconDeviceDesktop,\n\t\tlabel: \"hosts\",\n\t\titems: [\n\t\t\t{\n\t\t\t\tto: \"/nginx/proxy\",\n\t\t\t\tlabel: \"proxy-hosts\",\n\t\t\t\tpermissionSection: PROXY_HOSTS,\n\t\t\t\tpermission: VIEW,\n\t\t\t},\n\t\t\t{\n\t\t\t\tto: \"/nginx/redirection\",\n\t\t\t\tlabel: \"redirection-hosts\",\n\t\t\t\tpermissionSection: REDIRECTION_HOSTS,\n\t\t\t\tpermission: VIEW,\n\t\t\t},\n\t\t\t{\n\t\t\t\tto: \"/nginx/stream\",\n\t\t\t\tlabel: \"streams\",\n\t\t\t\tpermissionSection: STREAMS,\n\t\t\t\tpermission: VIEW,\n\t\t\t},\n\t\t\t{\n\t\t\t\tto: \"/nginx/404\",\n\t\t\t\tlabel: \"dead-hosts\",\n\t\t\t\tpermissionSection: DEAD_HOSTS,\n\t\t\t\tpermission: VIEW,\n\t\t\t},\n\t\t],\n\t},\n\t{\n\t\tto: \"/access\",\n\t\ticon: IconLock,\n\t\tlabel: \"access-lists\",\n\t\tpermissionSection: ACCESS_LISTS,\n\t\tpermission: VIEW,\n\t},\n\t{\n\t\tto: \"/certificates\",\n\t\ticon: IconShield,\n\t\tlabel: \"certificates\",\n\t\tpermissionSection: CERTIFICATES,\n\t\tpermission: VIEW,\n\t},\n\t{\n\t\tto: \"/users\",\n\t\ticon: IconUser,\n\t\tlabel: \"users\",\n\t\tpermissionSection: ADMIN,\n\t},\n\t{\n\t\tto: \"/audit-log\",\n\t\ticon: IconBook,\n\t\tlabel: \"auditlogs\",\n\t\tpermissionSection: ADMIN,\n\t},\n\t{\n\t\tto: \"/settings\",\n\t\ticon: IconSettings,\n\t\tlabel: \"settings\",\n\t\tpermissionSection: ADMIN,\n\t},\n];\n\nconst getMenuItem = (item: MenuItem, onClick?: () => void) => {\n\tif (item.items && item.items.length > 0) {\n\t\treturn getMenuDropown(item, onClick);\n\t}\n\n\treturn (\n\t\t<HasPermission\n\t\t\tkey={`item-${item.label}`}\n\t\t\tsection={item.permissionSection}\n\t\t\tpermission={item.permission || VIEW}\n\t\t\thideError\n\t\t>\n\t\t\t<li className=\"nav-item\">\n\t\t\t\t<NavLink to={item.to} onClick={onClick}>\n\t\t\t\t\t<span className=\"nav-link-icon d-md-none d-lg-inline-block\">\n\t\t\t\t\t\t{item.icon && React.createElement(item.icon, { height: 24, width: 24 })}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span className=\"nav-link-title\">\n\t\t\t\t\t\t<T id={item.label} />\n\t\t\t\t\t</span>\n\t\t\t\t</NavLink>\n\t\t\t</li>\n\t\t</HasPermission>\n\t);\n};\n\nconst getMenuDropown = (item: MenuItem, onClick?: () => void) => {\n\tconst cns = cn(\"nav-item\", \"dropdown\");\n\treturn (\n\t\t<HasPermission\n\t\t\tkey={`item-${item.label}`}\n\t\t\tsection={item.permissionSection}\n\t\t\tpermission={item.permission || VIEW}\n\t\t\thideError\n\t\t>\n\t\t\t<li className={cns}>\n\t\t\t\t<a\n\t\t\t\t\tclassName=\"nav-link dropdown-toggle\"\n\t\t\t\t\thref={item.to}\n\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\tdata-bs-auto-close=\"outside\"\n\t\t\t\t\taria-expanded=\"false\"\n\t\t\t\t\trole=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"nav-link-icon d-md-none d-lg-inline-block\">\n\t\t\t\t\t\t<IconDeviceDesktop height={24} width={24} />\n\t\t\t\t\t</span>\n\t\t\t\t\t<span className=\"nav-link-title\">\n\t\t\t\t\t\t<T id={item.label} />\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t\t<div className=\"dropdown-menu\">\n\t\t\t\t\t{item.items?.map((subitem, idx) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<HasPermission\n\t\t\t\t\t\t\t\tkey={`${idx}-${subitem.to}`}\n\t\t\t\t\t\t\t\tsection={subitem.permissionSection}\n\t\t\t\t\t\t\t\tpermission={subitem.permission || VIEW}\n\t\t\t\t\t\t\t\thideError\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<NavLink to={subitem.to} isDropdownItem onClick={onClick}>\n\t\t\t\t\t\t\t\t\t<T id={subitem.label} />\n\t\t\t\t\t\t\t\t</NavLink>\n\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</HasPermission>\n\t);\n};\n\nexport function SiteMenu() {\n\tconst closeMenu = () => setTimeout(() => {\n\t\tconst navbarToggler = document.querySelector<HTMLElement>(\".navbar-toggler\");\n\t\tconst navbarMenu = document.querySelector(\"#navbar-menu\");\n\t\tif (navbarToggler && navbarMenu?.classList.contains(\"show\")) {\n\t\t\tnavbarToggler.click();\n\t\t}\n\t}, 300);\n\n\treturn (\n\t\t<header className=\"navbar-expand-md\">\n\t\t\t<div className=\"collapse navbar-collapse\" id=\"navbar-menu\">\n\t\t\t\t<div className=\"navbar\">\n\t\t\t\t\t<div className=\"container-xl\">\n\t\t\t\t\t\t<div className=\"row flex-column flex-md-row flex-fill align-items-center\">\n\t\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t\t<ul className=\"navbar-nav\">\n\t\t\t\t\t\t\t\t\t{menuItems.length > 0 &&\n\t\t\t\t\t\t\t\t\t\tmenuItems.map((item) => {\n\t\t\t\t\t\t\t\t\t\t\treturn getMenuItem(item, closeMenu);\n\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</header>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/EmptyRow.tsx",
    "content": "import type { Table as ReactTable } from \"@tanstack/react-table\";\n\ninterface Props {\n\ttableInstance: ReactTable<any>;\n}\nfunction EmptyRow({ tableInstance }: Props) {\n\treturn (\n\t\t<tr>\n\t\t\t<td colSpan={tableInstance.getVisibleFlatColumns().length}>\n\t\t\t\t<p className=\"text-center\">There are no items</p>\n\t\t\t</td>\n\t\t</tr>\n\t);\n}\n\nexport { EmptyRow };\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/AccessListformatter.tsx",
    "content": "import type { AccessList } from \"src/api/backend\";\nimport { T } from \"src/locale\";\nimport { showAccessListModal } from \"src/modals\";\n\ninterface Props {\n\taccess?: AccessList;\n}\nexport function AccessListFormatter({ access }: Props) {\n\tif (!access) {\n\t\treturn <T id=\"public\" />;\n\t}\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclassName=\"btn btn-action btn-sm px-1\"\n\t\t\tonClick={(e) => {\n\t\t\t\te.preventDefault();\n\t\t\t\tshowAccessListModal(access?.id || 0);\n\t\t\t}}\n\t\t>\n\t\t\t{access.name}\n\t\t</button>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/CertificateFormatter.tsx",
    "content": "import type { Certificate } from \"src/api/backend\";\nimport { T } from \"src/locale\";\n\ninterface Props {\n\tcertificate?: Certificate;\n}\nexport function CertificateFormatter({ certificate }: Props) {\n\tlet translation = \"http-only\";\n\tif (certificate) {\n\t\ttranslation = certificate.provider;\n\t\tif (translation === \"letsencrypt\") {\n\t\t\ttranslation = \"lets-encrypt\";\n\t\t} else if (translation === \"other\") {\n\t\t\ttranslation = \"certificates.custom\";\n\t\t}\n\t}\n\treturn <T id={translation} />;\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx",
    "content": "import OverlayTrigger from \"react-bootstrap/OverlayTrigger\";\nimport Popover from \"react-bootstrap/Popover\";\nimport type { DeadHost, ProxyHost, RedirectionHost, Stream } from \"src/api/backend\";\nimport { TrueFalseFormatter } from \"src/components\";\nimport { T } from \"src/locale\";\n\nconst getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => {\n\tif (items.length === 0) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t<>\n\t\t\t<div>\n\t\t\t\t<strong>\n\t\t\t\t\t<T id={title} />\n\t\t\t\t</strong>\n\t\t\t</div>\n\t\t\t{items.map((host) => (\n\t\t\t\t<div key={host.id} className=\"ms-1\">\n\t\t\t\t\t{host.domainNames.join(\", \")}\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</>\n\t);\n};\n\nconst getSectionStream = (items: Stream[]) => {\n\tif (items.length === 0) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t<>\n\t\t\t<div>\n\t\t\t\t<strong>\n\t\t\t\t\t<T id=\"streams\" />\n\t\t\t\t</strong>\n\t\t\t</div>\n\t\t\t{items.map((stream) => (\n\t\t\t\t<div key={stream.id} className=\"ms-1\">\n\t\t\t\t\t{stream.forwardingHost}:{stream.forwardingPort}\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</>\n\t);\n};\n\ninterface Props {\n\tproxyHosts: ProxyHost[];\n\tredirectionHosts: RedirectionHost[];\n\tdeadHosts: DeadHost[];\n\tstreams: Stream[];\n}\nexport function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }: Props) {\n\tconst totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length + streams?.length;\n\tif (totalCount === 0) {\n\t\treturn <TrueFalseFormatter value={false} falseLabel=\"certificate.not-in-use\" />;\n\t}\n\n\tproxyHosts.sort();\n\tredirectionHosts.sort();\n\tdeadHosts.sort();\n\tstreams.sort();\n\n\tconst popover = (\n\t\t<Popover id=\"popover-basic\">\n\t\t\t<Popover.Body>\n\t\t\t\t{getSection(\"proxy-hosts\", proxyHosts)}\n\t\t\t\t{getSection(\"redirection-hosts\", redirectionHosts)}\n\t\t\t\t{getSection(\"dead-hosts\", deadHosts)}\n\t\t\t\t{getSectionStream(streams)}\n\t\t\t</Popover.Body>\n\t\t</Popover>\n\t);\n\n\treturn (\n\t\t<OverlayTrigger trigger={[\"hover\", \"click\", \"focus\"]} placement=\"bottom\" overlay={popover}>\n\t\t\t<div>\n\t\t\t\t<TrueFalseFormatter value trueLabel=\"certificate.in-use\" />\n\t\t\t</div>\n\t\t</OverlayTrigger>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/DateFormatter.tsx",
    "content": "import cn from \"classnames\";\nimport { differenceInDays, isPast } from \"date-fns\";\nimport { useLocaleState } from \"src/context\";\nimport { formatDateTime, parseDate } from \"src/locale\";\n\ninterface Props {\n\tvalue: string;\n\thighlightPast?: boolean;\n\thighlistNearlyExpired?: boolean;\n}\nexport function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {\n\tconst { locale } = useLocaleState();\n\tconst d = parseDate(value);\n\tconst dateIsPast = d ? isPast(d) : false;\n\tconst days = d ? differenceInDays(d, new Date()) : 0;\n\tconst cl = cn({\n\t\t\"text-danger\": highlightPast && dateIsPast,\n\t\t\"text-warning\": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,\n\t});\n\treturn <span className={cl}>{formatDateTime(value, locale)}</span>;\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/DomainsFormatter.tsx",
    "content": "import cn from \"classnames\";\nimport type { ReactNode } from \"react\";\nimport { useLocaleState } from \"src/context\";\nimport { formatDateTime, T } from \"src/locale\";\n\ninterface Props {\n\tdomains: string[];\n\tcreatedOn?: string;\n\tniceName?: string;\n\tprovider?: string;\n\tcolor?: string;\n}\n\nconst DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {\n\t// when domain contains a wildcard, make the link go nowhere.\n\t// Apparently the domain can be null or undefined sometimes.\n\t// This try is just a safeguard to prevent the whole formatter from breaking.\n\tif (!domain) return null;\n\ttry {\n\t\tlet onClick: ((e: React.MouseEvent) => void) | undefined;\n\t\tif (domain.includes(\"*\")) {\n\t\t\tonClick = (e: React.MouseEvent) => e.preventDefault();\n\t\t}\n\t\treturn (\n\t\t\t<a\n\t\t\t\tkey={domain}\n\t\t\t\thref={`http://${domain}`}\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tonClick={onClick}\n\t\t\t\tclassName={cn(\"badge\", color ? `bg-${color}-lt` : null, \"domain-name\", \"me-2\")}\n\t\t\t>\n\t\t\t\t{domain}\n\t\t\t</a>\n\t\t);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nexport function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {\n\tconst { locale } = useLocaleState();\n\tconst elms: ReactNode[] = [];\n\n\tif ((!domains || domains.length === 0) && !niceName) {\n\t\telms.push(\n\t\t\t<span key=\"nice-name\" className=\"badge bg-danger-lt me-2\">\n\t\t\t\tUnknown\n\t\t\t</span>,\n\t\t);\n\t}\n\tif (!domains || (niceName && provider !== \"letsencrypt\")) {\n\t\telms.push(\n\t\t\t<span key=\"nice-name\" className=\"badge bg-info-lt me-2\">\n\t\t\t\t{niceName}\n\t\t\t</span>,\n\t\t);\n\t}\n\n\tif (domains) {\n\t\tdomains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />));\n\t}\n\n\treturn (\n\t\t<div className=\"flex-fill\">\n\t\t\t<div className=\"font-weight-medium\">{...elms}</div>\n\t\t\t{createdOn ? (\n\t\t\t\t<div className=\"text-secondary mt-1\">\n\t\t\t\t\t<T id=\"created-on\" data={{ date: formatDateTime(createdOn, locale) }} />\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/EmailFormatter.tsx",
    "content": "interface Props {\n\temail: string;\n}\nexport function EmailFormatter({ email }: Props) {\n\treturn (\n\t\t<a href={`mailto:${email}`} className=\"badge bg-yellow-lt\">\n\t\t\t{email}\n\t\t</a>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/EventFormatter.tsx",
    "content": "import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport type { AuditLog } from \"src/api/backend\";\nimport { useLocaleState } from \"src/context\";\nimport { formatDateTime, T } from \"src/locale\";\n\nconst getEventValue = (event: AuditLog) => {\n\tswitch (event.objectType) {\n\t\tcase \"access-list\":\n\t\tcase \"user\":\n\t\t\treturn event.meta?.name;\n\t\tcase \"proxy-host\":\n\t\tcase \"redirection-host\":\n\t\tcase \"dead-host\":\n\t\t\treturn event.meta?.domainNames?.join(\", \") || \"N/A\";\n\t\tcase \"stream\":\n\t\t\treturn event.meta?.incomingPort || \"N/A\";\n\t\tcase \"certificate\":\n\t\t\treturn event.meta?.domainNames?.join(\", \") || event.meta?.niceName || \"N/A\";\n\t\tdefault:\n\t\t\treturn `UNKNOWN EVENT TYPE: ${event.objectType}`;\n\t}\n};\n\nconst getColorForAction = (action: string) => {\n\tswitch (action) {\n\t\tcase \"created\":\n\t\t\treturn \"text-lime\";\n\t\tcase \"deleted\":\n\t\t\treturn \"text-red\";\n\t\tdefault:\n\t\t\treturn \"text-blue\";\n\t}\n};\n\nconst getIcon = (row: AuditLog) => {\n\tconst c = cn(getColorForAction(row.action), \"me-1\");\n\tlet ico = null;\n\tswitch (row.objectType) {\n\t\tcase \"user\":\n\t\t\tico = <IconUser size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"proxy-host\":\n\t\t\tico = <IconBolt size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"redirection-host\":\n\t\t\tico = <IconArrowsCross size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"dead-host\":\n\t\t\tico = <IconBoltOff size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"stream\":\n\t\t\tico = <IconDisc size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"access-list\":\n\t\t\tico = <IconLock size={16} className={c} />;\n\t\t\tbreak;\n\t\tcase \"certificate\":\n\t\t\tico = <IconShield size={16} className={c} />;\n\t\t\tbreak;\n\t}\n\n\treturn ico;\n};\n\ninterface Props {\n\trow: AuditLog;\n}\nexport function EventFormatter({ row }: Props) {\n\tconst { locale } = useLocaleState();\n\treturn (\n\t\t<div className=\"flex-fill\">\n\t\t\t<div className=\"font-weight-medium\">\n\t\t\t\t{getIcon(row)}\n\t\t\t\t<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />\n\t\t\t\t&nbsp; &mdash; <span className=\"badge\">{getEventValue(row)}</span>\n\t\t\t</div>\n\t\t\t<div className=\"text-secondary mt-1\">{formatDateTime(row.createdOn, locale)}</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/GravatarFormatter.tsx",
    "content": "const defaultImg = \"/images/default-avatar.jpg\";\n\ninterface Props {\n\turl?: string;\n\tname?: string;\n}\nexport function GravatarFormatter({ url, name }: Props) {\n\treturn (\n\t\t<div className=\"d-flex py-1 align-items-center\">\n\t\t\t<span\n\t\t\t\ttitle={name}\n\t\t\t\tclassName=\"avatar avatar-2 me-2\"\n\t\t\t\tstyle={{\n\t\t\t\t\tbackgroundImage: `url(${url || defaultImg})`,\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/RolesFormatter.tsx",
    "content": "import { T } from \"src/locale\";\n\ninterface Props {\n\troles: string[];\n}\nexport function RolesFormatter({ roles }: Props) {\n\tconst r = roles || [];\n\tif (r.length === 0) {\n\t\tr[0] = \"standard-user\";\n\t}\n\treturn (\n\t\t<>\n\t\t\t{r.map((role: string) => (\n\t\t\t\t<span key={role} className=\"badge bg-yellow-lt me-1\">\n\t\t\t\t\t<T id={`role.${role}`} />\n\t\t\t\t</span>\n\t\t\t))}\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx",
    "content": "import cn from \"classnames\";\nimport { T } from \"src/locale\";\n\ninterface Props {\n\tvalue: boolean;\n\ttrueLabel?: string;\n\ttrueColor?: string;\n\tfalseLabel?: string;\n\tfalseColor?: string;\n}\nexport function TrueFalseFormatter({\n\tvalue,\n\ttrueLabel = \"enabled\",\n\ttrueColor = \"lime\",\n\tfalseLabel = \"disabled\",\n\tfalseColor = \"red\",\n}: Props) {\n\treturn (\n\t\t<span className={cn(\"status\", `status-${value ? trueColor : falseColor}`)}>\n\t\t\t<span className=\"status-dot status-dot-animated\" />\n\t\t\t<T id={value ? trueLabel : falseLabel} />\n\t\t</span>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx",
    "content": "import { useLocaleState } from \"src/context\";\nimport { formatDateTime, T } from \"src/locale\";\n\ninterface Props {\n\tvalue: string;\n\tcreatedOn?: string;\n\tdisabled?: boolean;\n}\nexport function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {\n\tconst { locale } = useLocaleState();\n\treturn (\n\t\t<div className=\"flex-fill\">\n\t\t\t<div className=\"font-weight-medium\">\n\t\t\t\t<div className={`font-weight-medium ${disabled ? \"text-red\" : \"\"}`}>{value}</div>\n\t\t\t</div>\n\t\t\t{createdOn ? (\n\t\t\t\t<div className={`text-secondary mt-1 ${disabled ? \"text-red\" : \"\"}`}>\n\t\t\t\t\t<T id={disabled ? \"disabled\" : \"created-on\"} data={{ date: formatDateTime(createdOn, locale) }} />\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/Table/Formatter/index.ts",
    "content": "export * from \"./AccessListformatter\";\nexport * from \"./CertificateFormatter\";\nexport * from \"./CertificateInUseFormatter\";\nexport * from \"./DateFormatter\";\nexport * from \"./DomainsFormatter\";\nexport * from \"./EmailFormatter\";\nexport * from \"./EventFormatter\";\nexport * from \"./GravatarFormatter\";\nexport * from \"./RolesFormatter\";\nexport * from \"./TrueFalseFormatter\";\nexport * from \"./ValueWithDateFormatter\";\n"
  },
  {
    "path": "frontend/src/components/Table/TableBody.tsx",
    "content": "import { flexRender } from \"@tanstack/react-table\";\nimport type { TableLayoutProps } from \"src/components\";\nimport { EmptyRow } from \"./EmptyRow\";\n\nfunction TableBody<T>(props: TableLayoutProps<T>) {\n\tconst { tableInstance, extraStyles, emptyState } = props;\n\tconst rows = tableInstance.getRowModel().rows;\n\n\tif (rows.length === 0) {\n\t\treturn (\n\t\t\t<tbody className=\"table-tbody\">\n\t\t\t\t{emptyState ? emptyState : <EmptyRow tableInstance={tableInstance} />}\n\t\t\t</tbody>\n\t\t);\n\t}\n\n\treturn (\n\t\t<tbody className=\"table-tbody\">\n\t\t\t{rows.map((row: any) => {\n\t\t\t\treturn (\n\t\t\t\t\t<tr key={row.id} {...extraStyles?.row(row.original)}>\n\t\t\t\t\t\t{row.getVisibleCells().map((cell: any) => {\n\t\t\t\t\t\t\tconst { className } = (cell.column.columnDef.meta as any) ?? {};\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<td key={cell.id} className={className}>\n\t\t\t\t\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</tr>\n\t\t\t\t);\n\t\t\t})}\n\t\t</tbody>\n\t);\n}\n\nexport { TableBody };\n"
  },
  {
    "path": "frontend/src/components/Table/TableHeader.tsx",
    "content": "import type { TableLayoutProps } from \"src/components\";\n\nfunction TableHeader<T>(props: TableLayoutProps<T>) {\n\tconst { tableInstance } = props;\n\tconst headerGroups = tableInstance.getHeaderGroups();\n\n\treturn (\n\t\t<thead>\n\t\t\t{headerGroups.map((headerGroup: any) => (\n\t\t\t\t<tr key={headerGroup.id}>\n\t\t\t\t\t{headerGroup.headers.map((header: any) => {\n\t\t\t\t\t\tconst { column } = header;\n\t\t\t\t\t\tconst { className } = (column.columnDef.meta as any) ?? {};\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<th key={header.id} className={className}>\n\t\t\t\t\t\t\t\t{typeof column.columnDef.header === \"string\" ? `${column.columnDef.header}` : null}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</tr>\n\t\t\t))}\n\t\t</thead>\n\t);\n}\n\nexport { TableHeader };\n"
  },
  {
    "path": "frontend/src/components/Table/TableHelpers.ts",
    "content": "export interface TablePagination {\n\tlimit: number;\n\toffset: number;\n\ttotal: number;\n}\n\nexport interface TableSortBy {\n\tid: string;\n\tdesc: boolean;\n}\n\nexport interface TableFilter {\n\tid: string;\n\tvalue: any;\n}\n\nconst tableEvents = {\n\tFILTERS_CHANGED: \"FILTERS_CHANGED\",\n\tPAGE_CHANGED: \"PAGE_CHANGED\",\n\tPAGE_SIZE_CHANGED: \"PAGE_SIZE_CHANGED\",\n\tTOTAL_COUNT_CHANGED: \"TOTAL_COUNT_CHANGED\",\n\tSORT_CHANGED: \"SORT_CHANGED\",\n};\n\nconst tableEventReducer = (state: any, { type, payload }: any) => {\n\tlet offset = state.offset;\n\tswitch (type) {\n\t\tcase tableEvents.PAGE_CHANGED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\toffset: payload * state.limit,\n\t\t\t};\n\t\tcase tableEvents.PAGE_SIZE_CHANGED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tlimit: payload,\n\t\t\t};\n\t\tcase tableEvents.TOTAL_COUNT_CHANGED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttotal: payload,\n\t\t\t};\n\t\tcase tableEvents.SORT_CHANGED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tsortBy: payload,\n\t\t\t};\n\t\tcase tableEvents.FILTERS_CHANGED:\n\t\t\tif (state.filters !== payload) {\n\t\t\t\t// this actually was a legit change\n\t\t\t\t// sets to page 1 when filter is modified\n\t\t\t\toffset = 0;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tfilters: payload,\n\t\t\t\toffset,\n\t\t\t};\n\t\tdefault:\n\t\t\tthrow new Error(`Unhandled action type: ${type}`);\n\t}\n};\n\nexport { tableEvents, tableEventReducer };\n"
  },
  {
    "path": "frontend/src/components/Table/TableLayout.tsx",
    "content": "import type { Table as ReactTable } from \"@tanstack/react-table\";\nimport { TableBody } from \"./TableBody\";\nimport { TableHeader } from \"./TableHeader\";\n\ninterface TableLayoutProps<TFields> {\n\ttableInstance: ReactTable<TFields>;\n\temptyState?: React.ReactNode;\n\textraStyles?: {\n\t\trow: (rowData: TFields) => any | undefined;\n\t};\n}\nfunction TableLayout<TFields>(props: TableLayoutProps<TFields>) {\n\tconst hasRows = props.tableInstance.getRowModel().rows.length > 0;\n\treturn (\n\t\t<div className=\"table-responsive\">\n\t\t\t<table className=\"table table-vcenter table-selectable mb-0\">\n\t\t\t\t{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null}\n\t\t\t\t<TableBody {...props} />\n\t\t\t</table>\n\t\t</div>\n\t);\n}\n\nexport { TableLayout, type TableLayoutProps };\n"
  },
  {
    "path": "frontend/src/components/Table/index.ts",
    "content": "export * from \"./Formatter\";\nexport * from \"./TableHeader\";\nexport * from \"./TableHelpers\";\nexport * from \"./TableLayout\";\n"
  },
  {
    "path": "frontend/src/components/ThemeSwitcher.module.css",
    "content": ".darkBtn {\n\tcolor: var(--tblr-light) !important;\n\t&:hover {\n\t\tborder: var(--tblr-btn-border-width) solid transparent !important;\n\t\tbackground: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;\n\t}\n}\n\n.lightBtn {\n\tcolor: var(--tblr-dark) !important;\n\t&:hover {\n\t\tborder: var(--tblr-btn-border-width) solid transparent !important;\n\t\tbackground: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;\n\t}\n}\n"
  },
  {
    "path": "frontend/src/components/ThemeSwitcher.tsx",
    "content": "import { IconMoon, IconSun } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport { Button } from \"src/components\";\nimport { useTheme } from \"src/hooks\";\nimport styles from \"./ThemeSwitcher.module.css\";\n\ninterface Props {\n\tclassName?: string;\n}\nfunction ThemeSwitcher({ className }: Props) {\n\tconst { setTheme } = useTheme();\n\n\treturn (\n\t\t<div className={cn(\"d-print-none\", \"d-inline-block\", className)}>\n\t\t\t<Button\n\t\t\t\tsize=\"sm\"\n\t\t\t\tclassName={cn(\"btn-ghost-dark\", \"hide-theme-dark\", styles.lightBtn)}\n\t\t\t\tdata-bs-toggle=\"tooltip\"\n\t\t\t\tdata-bs-placement=\"bottom\"\n\t\t\t\taria-label=\"Enable dark mode\"\n\t\t\t\tdata-bs-original-title=\"Enable dark mode\"\n\t\t\t\tonClick={() => setTheme(\"dark\")}\n\t\t\t>\n\t\t\t\t<IconMoon width={24} />\n\t\t\t</Button>\n\t\t\t<Button\n\t\t\t\tsize=\"sm\"\n\t\t\t\tclassName={cn(\"btn-ghost-light\", \"hide-theme-light\", styles.darkBtn)}\n\t\t\t\tdata-bs-toggle=\"tooltip\"\n\t\t\t\tdata-bs-placement=\"bottom\"\n\t\t\t\taria-label=\"Enable dark mode\"\n\t\t\t\tdata-bs-original-title=\"Enable dark mode\"\n\t\t\t\tonClick={() => setTheme(\"light\")}\n\t\t\t>\n\t\t\t\t<IconSun width={24} />\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n\nexport { ThemeSwitcher };\n"
  },
  {
    "path": "frontend/src/components/Unhealthy.tsx",
    "content": "import { Page } from \"src/components\";\n\nexport function Unhealthy() {\n\treturn (\n\t\t<Page className=\"page-center\">\n\t\t\t<div className=\"container-tight py-4\">\n\t\t\t\t<div className=\"empty\">\n\t\t\t\t\t<div className=\"empty-img\">\n\t\t\t\t\t\t<img src=\"/images/unhealthy.svg\" alt=\"\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<p className=\"empty-title\">The API is not healthy.</p>\n\t\t\t\t\t<p className=\"empty-subtitle text-secondary\">We'll keep checking and hope to be back soon!</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Page>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/components/index.ts",
    "content": "export * from \"./Button\";\nexport * from \"./EmptyData\";\nexport * from \"./ErrorNotFound\";\nexport * from \"./Flag\";\nexport * from \"./Form\";\nexport * from \"./HasPermission\";\nexport * from \"./Loading\";\nexport * from \"./LoadingPage\";\nexport * from \"./LocalePicker\";\nexport * from \"./NavLink\";\nexport * from \"./Page\";\nexport * from \"./SiteContainer\";\nexport * from \"./SiteFooter\";\nexport * from \"./SiteHeader\";\nexport * from \"./SiteMenu\";\nexport * from \"./Table\";\nexport * from \"./ThemeSwitcher\";\nexport * from \"./Unhealthy\";\n"
  },
  {
    "path": "frontend/src/context/AuthContext.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { createContext, type ReactNode, useContext, useState } from \"react\";\nimport { useIntervalWhen } from \"rooks\";\nimport {\n\tgetToken,\n\tisTwoFactorChallenge,\n\tloginAsUser,\n\trefreshToken,\n\tverify2FA,\n\ttype TokenResponse,\n} from \"src/api/backend\";\nimport AuthStore from \"src/modules/AuthStore\";\n\n// 2FA challenge state\nexport interface TwoFactorChallenge {\n\tchallengeToken: string;\n}\n\n// Context\nexport interface AuthContextType {\n\tauthenticated: boolean;\n\ttwoFactorChallenge: TwoFactorChallenge | null;\n\tlogin: (username: string, password: string) => Promise<void>;\n\tverifyTwoFactor: (code: string) => Promise<void>;\n\tcancelTwoFactor: () => void;\n\tloginAs: (id: number) => Promise<void>;\n\tlogout: () => void;\n\ttoken?: string;\n}\n\nconst initalValue = null;\nconst AuthContext = createContext<AuthContextType | null>(initalValue);\n\n// Provider\ninterface Props {\n\tchildren?: ReactNode;\n\ttokenRefreshInterval?: number;\n}\nfunction AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) {\n\tconst queryClient = useQueryClient();\n\tconst [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken());\n\tconst [twoFactorChallenge, setTwoFactorChallenge] = useState<TwoFactorChallenge | null>(null);\n\n\tconst handleTokenUpdate = (response: TokenResponse) => {\n\t\tAuthStore.set(response);\n\t\tsetAuthenticated(true);\n\t\tsetTwoFactorChallenge(null);\n\t};\n\n\tconst login = async (identity: string, secret: string) => {\n\t\tconst response = await getToken(identity, secret);\n\t\tif (isTwoFactorChallenge(response)) {\n\t\t\tsetTwoFactorChallenge({ challengeToken: response.challengeToken });\n\t\t\treturn;\n\t\t}\n\t\thandleTokenUpdate(response);\n\t};\n\n\tconst verifyTwoFactor = async (code: string) => {\n\t\tif (!twoFactorChallenge) {\n\t\t\tthrow new Error(\"No 2FA challenge pending\");\n\t\t}\n\t\tconst response = await verify2FA(twoFactorChallenge.challengeToken, code);\n\t\thandleTokenUpdate(response);\n\t};\n\n\tconst cancelTwoFactor = () => {\n\t\tsetTwoFactorChallenge(null);\n\t};\n\n\tconst loginAs = async (id: number) => {\n\t\tconst response = await loginAsUser(id);\n\t\tAuthStore.add(response);\n\t\tqueryClient.clear();\n\t\twindow.location.reload();\n\t};\n\n\tconst logout = () => {\n\t\tif (AuthStore.count() >= 2) {\n\t\t\tAuthStore.drop();\n\t\t\tqueryClient.clear();\n\t\t\twindow.location.reload();\n\t\t\treturn;\n\t\t}\n\t\tAuthStore.clear();\n\t\tsetAuthenticated(false);\n\t\tqueryClient.clear();\n\t};\n\n\tconst refresh = async () => {\n\t\tconst response = await refreshToken();\n\t\thandleTokenUpdate(response);\n\t};\n\n\tuseIntervalWhen(\n\t\t() => {\n\t\t\tif (authenticated) {\n\t\t\t\trefresh();\n\t\t\t}\n\t\t},\n\t\ttokenRefreshInterval,\n\t\ttrue,\n\t);\n\n\tconst value = {\n\t\tauthenticated,\n\t\ttwoFactorChallenge,\n\t\tlogin,\n\t\tverifyTwoFactor,\n\t\tcancelTwoFactor,\n\t\tloginAs,\n\t\tlogout,\n\t};\n\n\treturn <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nfunction useAuthState() {\n\tconst context = useContext(AuthContext);\n\tif (!context) {\n\t\tthrow new Error(\"useAuthState must be used within a AuthProvider\");\n\t}\n\treturn context;\n}\n\nexport { AuthProvider, useAuthState };\nexport default AuthContext;\n"
  },
  {
    "path": "frontend/src/context/LocaleContext.tsx",
    "content": "import { createContext, type ReactNode, useContext, useState } from \"react\";\nimport { getLocale } from \"src/locale\";\n\n// Context\nexport interface LocaleContextType {\n\tsetLocale: (locale: string) => void;\n\tlocale?: string;\n}\n\nconst initalValue = null;\nconst LocaleContext = createContext<LocaleContextType | null>(initalValue);\n\n// Provider\ninterface Props {\n\tchildren?: ReactNode;\n}\nfunction LocaleProvider({ children }: Props) {\n\tconst [locale, setLocaleValue] = useState(getLocale());\n\n\tconst setLocale = async (locale: string) => {\n\t\tsetLocaleValue(locale);\n\t};\n\n\tconst value = { locale, setLocale };\n\n\treturn <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;\n}\n\nfunction useLocaleState() {\n\tconst context = useContext(LocaleContext);\n\tif (!context) {\n\t\tthrow new Error(\"useLocaleState must be used within a LocaleProvider\");\n\t}\n\treturn context;\n}\n\nexport { LocaleProvider, useLocaleState };\nexport default LocaleContext;\n"
  },
  {
    "path": "frontend/src/context/ThemeContext.tsx",
    "content": "import type React from \"react\";\nimport { createContext, type ReactNode, useContext, useEffect, useState } from \"react\";\n\nconst StorageKey = \"tabler-theme\";\nexport const Light = \"light\";\nexport const Dark = \"dark\";\n\n// Define theme types\nexport type Theme = \"light\" | \"dark\";\n\ninterface ThemeContextType {\n\ttheme: Theme;\n\ttoggleTheme: () => void;\n\tsetTheme: (theme: Theme) => void;\n\tgetTheme: () => Theme;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\ninterface ThemeProviderProps {\n\tchildren: ReactNode;\n}\n\nconst getBrowserDefault = (): Theme => {\n\tif (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n\t\treturn Dark;\n\t}\n\treturn Light;\n};\n\nexport const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {\n\tconst [theme, setThemeState] = useState<Theme>(() => {\n\t\t// Try to read theme from localStorage or use 'light' as default\n\t\tif (typeof window !== \"undefined\") {\n\t\t\tconst stored = localStorage.getItem(StorageKey) as Theme | null;\n\t\t\treturn stored || getBrowserDefault();\n\t\t}\n\t\treturn getBrowserDefault();\n\t});\n\n\tuseEffect(() => {\n\t\tdocument.body.dataset.theme = theme;\n\t\tdocument.body.classList.remove(theme === Light ? Dark : Light);\n\t\tdocument.body.classList.add(theme);\n\t\tlocalStorage.setItem(StorageKey, theme);\n\t}, [theme]);\n\n\tconst toggleTheme = () => {\n\t\tsetThemeState((prev) => (prev === Light ? Dark : Light));\n\t};\n\n\tconst setTheme = (newTheme: Theme) => {\n\t\tsetThemeState(newTheme);\n\t};\n\n\tconst getTheme = () => {\n\t\treturn theme;\n\t};\n\n\tdocument.documentElement.setAttribute(\"data-bs-theme\", theme);\n\treturn <ThemeContext.Provider value={{ theme, toggleTheme, setTheme, getTheme }}>{children}</ThemeContext.Provider>;\n};\n\nexport function useTheme(): ThemeContextType {\n\tconst context = useContext(ThemeContext);\n\tif (!context) {\n\t\tthrow new Error(\"useTheme must be used within a ThemeProvider\");\n\t}\n\treturn context;\n}\n"
  },
  {
    "path": "frontend/src/context/index.ts",
    "content": "export * from \"./AuthContext\";\nexport * from \"./LocaleContext\";\nexport * from \"./ThemeContext\";\n"
  },
  {
    "path": "frontend/src/declarations.d.ts",
    "content": "declare module \"*.md\";\n"
  },
  {
    "path": "frontend/src/hooks/index.ts",
    "content": "export * from \"./useAccessList\";\nexport * from \"./useAccessLists\";\nexport * from \"./useAuditLog\";\nexport * from \"./useAuditLogs\";\nexport * from \"./useCertificate\";\nexport * from \"./useCertificates\";\nexport * from \"./useCheckVersion\";\nexport * from \"./useDeadHost\";\nexport * from \"./useDeadHosts\";\nexport * from \"./useDnsProviders\";\nexport * from \"./useHealth\";\nexport * from \"./useHostReport\";\nexport * from \"./useProxyHost\";\nexport * from \"./useProxyHosts\";\nexport * from \"./useRedirectionHost\";\nexport * from \"./useRedirectionHosts\";\nexport * from \"./useSetting\";\nexport * from \"./useStream\";\nexport * from \"./useStreams\";\nexport * from \"./useTheme\";\nexport * from \"./useUser\";\nexport * from \"./useUsers\";\n"
  },
  {
    "path": "frontend/src/hooks/useAccessList.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n\ttype AccessList,\n\ttype AccessListExpansion,\n\tcreateAccessList,\n\tgetAccessList,\n\tupdateAccessList,\n} from \"src/api/backend\";\n\nconst fetchAccessList = (id: number | \"new\", expand: AccessListExpansion[] = [\"owner\"]) => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\townerUserId: 0,\n\t\t\tname: \"\",\n\t\t\tsatisfyAny: false,\n\t\t\tpassAuth: false,\n\t\t\tmeta: {},\n\t\t} as AccessList);\n\t}\n\treturn getAccessList(id, expand);\n};\n\nconst useAccessList = (id: number | \"new\", expand?: AccessListExpansion[], options = {}) => {\n\treturn useQuery<AccessList, Error>({\n\t\tqueryKey: [\"access-list\", id, expand],\n\t\tqueryFn: () => fetchAccessList(id, expand),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetAccessList = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: AccessList) => (values.id ? updateAccessList(values) : createAccessList(values)),\n\t\tonMutate: (values: AccessList) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"access-list\", values.id]);\n\t\t\tqueryClient.setQueryData([\"access-list\", values.id], (old: AccessList) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"access-list\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: AccessList) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"access-list\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"access-lists\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"proxy-hosts\"] });\n\t\t},\n\t});\n};\n\nexport { useAccessList, useSetAccessList };\n"
  },
  {
    "path": "frontend/src/hooks/useAccessLists.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type AccessList, type AccessListExpansion, getAccessLists } from \"src/api/backend\";\n\nconst fetchAccessLists = (expand?: AccessListExpansion[]) => {\n\treturn getAccessLists(expand);\n};\n\nconst useAccessLists = (expand?: AccessListExpansion[], options = {}) => {\n\treturn useQuery<AccessList[], Error>({\n\t\tqueryKey: [\"access-lists\", { expand }],\n\t\tqueryFn: () => fetchAccessLists(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchAccessLists, useAccessLists };\n"
  },
  {
    "path": "frontend/src/hooks/useAuditLog.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type AuditLog, getAuditLog } from \"src/api/backend\";\n\nconst fetchAuditLog = (id: number) => {\n\treturn getAuditLog(id, [\"user\"]);\n};\n\nconst useAuditLog = (id: number, options = {}) => {\n\treturn useQuery<AuditLog, Error>({\n\t\tqueryKey: [\"audit-log\", id],\n\t\tqueryFn: () => fetchAuditLog(id),\n\t\tstaleTime: 5 * 60 * 1000, // 5 minutes\n\t\t...options,\n\t});\n};\n\nexport { useAuditLog };\n"
  },
  {
    "path": "frontend/src/hooks/useAuditLogs.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type AuditLog, type AuditLogExpansion, getAuditLogs } from \"src/api/backend\";\n\nconst fetchAuditLogs = (expand?: AuditLogExpansion[]) => {\n\treturn getAuditLogs(expand);\n};\n\nconst useAuditLogs = (expand?: AuditLogExpansion[], options = {}) => {\n\treturn useQuery<AuditLog[], Error>({\n\t\tqueryKey: [\"audit-logs\", { expand }],\n\t\tqueryFn: () => fetchAuditLogs(expand),\n\t\tstaleTime: 10 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchAuditLogs, useAuditLogs };\n"
  },
  {
    "path": "frontend/src/hooks/useCertificate.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type Certificate, getCertificate } from \"src/api/backend\";\n\nconst fetchCertificate = (id: number) => {\n\treturn getCertificate(id, [\"owner\"]);\n};\n\nconst useCertificate = (id: number, options = {}) => {\n\treturn useQuery<Certificate, Error>({\n\t\tqueryKey: [\"certificate\", id],\n\t\tqueryFn: () => fetchCertificate(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nexport { useCertificate };\n"
  },
  {
    "path": "frontend/src/hooks/useCertificates.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type Certificate, type CertificateExpansion, getCertificates } from \"src/api/backend\";\n\nconst fetchCertificates = (expand?: CertificateExpansion[]) => {\n\treturn getCertificates(expand);\n};\n\nconst useCertificates = (expand?: CertificateExpansion[], options = {}) => {\n\treturn useQuery<Certificate[], Error>({\n\t\tqueryKey: [\"certificates\", { expand }],\n\t\tqueryFn: () => fetchCertificates(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchCertificates, useCertificates };\n"
  },
  {
    "path": "frontend/src/hooks/useCheckVersion.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { checkVersion, type VersionCheckResponse } from \"src/api/backend\";\n\nconst fetchVersion = () => checkVersion();\n\nconst useCheckVersion = (options = {}) => {\n\treturn useQuery<VersionCheckResponse, Error>({\n\t\tqueryKey: [\"version-check\"],\n\t\tqueryFn: fetchVersion,\n\t\trefetchOnWindowFocus: false,\n\t\tretry: 5,\n\t\trefetchInterval: 30 * 1000, // 30 seconds\n\t\tstaleTime: 5 * 60 * 1000, // 5 mins\n\t\t...options,\n\t});\n};\n\nexport { fetchVersion, useCheckVersion };\n"
  },
  {
    "path": "frontend/src/hooks/useDeadHost.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createDeadHost, type DeadHost, getDeadHost, updateDeadHost } from \"src/api/backend\";\n\nconst fetchDeadHost = (id: number | \"new\") => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\townerUserId: 0,\n\t\t\tdomainNames: [],\n\t\t\tcertificateId: 0,\n\t\t\tsslForced: false,\n\t\t\tadvancedConfig: \"\",\n\t\t\tmeta: {},\n\t\t\thttp2Support: false,\n\t\t\tenabled: true,\n\t\t\thstsEnabled: false,\n\t\t\thstsSubdomains: false,\n\t\t} as DeadHost);\n\t}\n\treturn getDeadHost(id, [\"owner\"]);\n};\n\nconst useDeadHost = (id: number | \"new\", options = {}) => {\n\treturn useQuery<DeadHost, Error>({\n\t\tqueryKey: [\"dead-host\", id],\n\t\tqueryFn: () => fetchDeadHost(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetDeadHost = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: DeadHost) => (values.id ? updateDeadHost(values) : createDeadHost(values)),\n\t\tonMutate: (values: DeadHost) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"dead-host\", values.id]);\n\t\t\tqueryClient.setQueryData([\"dead-host\", values.id], (old: DeadHost) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"dead-host\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: DeadHost) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"dead-host\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"dead-hosts\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"host-report\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\t},\n\t});\n};\n\nexport { useDeadHost, useSetDeadHost };\n"
  },
  {
    "path": "frontend/src/hooks/useDeadHosts.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type DeadHost, getDeadHosts, type HostExpansion } from \"src/api/backend\";\n\nconst fetchDeadHosts = (expand?: HostExpansion[]) => {\n\treturn getDeadHosts(expand);\n};\n\nconst useDeadHosts = (expand?: HostExpansion[], options = {}) => {\n\treturn useQuery<DeadHost[], Error>({\n\t\tqueryKey: [\"dead-hosts\", { expand }],\n\t\tqueryFn: () => fetchDeadHosts(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchDeadHosts, useDeadHosts };\n"
  },
  {
    "path": "frontend/src/hooks/useDnsProviders.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { type DNSProvider, getCertificateDNSProviders } from \"src/api/backend\";\n\nconst fetchDnsProviders = () => {\n\treturn getCertificateDNSProviders();\n};\n\nconst useDnsProviders = (options = {}) => {\n\treturn useQuery<DNSProvider[], Error>({\n\t\tqueryKey: [\"dns-providers\"],\n\t\tqueryFn: () => fetchDnsProviders(),\n\t\tstaleTime: 300 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchDnsProviders, useDnsProviders };\n"
  },
  {
    "path": "frontend/src/hooks/useHealth.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getHealth, type HealthResponse } from \"src/api/backend\";\n\nconst fetchHealth = () => getHealth();\n\nconst useHealth = (options = {}) => {\n\treturn useQuery<HealthResponse, Error>({\n\t\tqueryKey: [\"health\"],\n\t\tqueryFn: fetchHealth,\n\t\trefetchOnWindowFocus: false,\n\t\tretry: 5,\n\t\trefetchInterval: 15 * 1000, // 15 seconds\n\t\tstaleTime: 14 * 1000, // 14 seconds\n\t\t...options,\n\t});\n};\n\nexport { fetchHealth, useHealth };\n"
  },
  {
    "path": "frontend/src/hooks/useHostReport.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getHostsReport } from \"src/api/backend\";\n\nconst fetchHostReport = () => getHostsReport();\n\nconst useHostReport = (options = {}) => {\n\treturn useQuery<Record<string, number>, Error>({\n\t\tqueryKey: [\"host-report\"],\n\t\tqueryFn: fetchHostReport,\n\t\trefetchOnWindowFocus: false,\n\t\tretry: 5,\n\t\trefetchInterval: 15 * 1000, // 15 seconds\n\t\tstaleTime: 14 * 1000, // 14 seconds\n\t\t...options,\n\t});\n};\n\nexport { fetchHostReport, useHostReport };\n"
  },
  {
    "path": "frontend/src/hooks/useProxyHost.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createProxyHost, getProxyHost, type ProxyHost, updateProxyHost } from \"src/api/backend\";\n\nconst fetchProxyHost = (id: number | \"new\") => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\townerUserId: 0,\n\t\t\tdomainNames: [],\n\t\t\tforwardHost: \"\",\n\t\t\tforwardPort: 0,\n\t\t\taccessListId: 0,\n\t\t\tcertificateId: 0,\n\t\t\tsslForced: false,\n\t\t\tcachingEnabled: false,\n\t\t\tblockExploits: false,\n\t\t\tadvancedConfig: \"\",\n\t\t\tmeta: {},\n\t\t\tallowWebsocketUpgrade: false,\n\t\t\thttp2Support: false,\n\t\t\tforwardScheme: \"\",\n\t\t\tenabled: true,\n\t\t\thstsEnabled: false,\n\t\t\thstsSubdomains: false,\n\t\t\ttrustForwardedProto: false,\n\t\t} as ProxyHost);\n\t}\n\treturn getProxyHost(id, [\"owner\"]);\n};\n\nconst useProxyHost = (id: number | \"new\", options = {}) => {\n\treturn useQuery<ProxyHost, Error>({\n\t\tqueryKey: [\"proxy-host\", id],\n\t\tqueryFn: () => fetchProxyHost(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetProxyHost = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: ProxyHost) => (values.id ? updateProxyHost(values) : createProxyHost(values)),\n\t\tonMutate: (values: ProxyHost) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"proxy-host\", values.id]);\n\t\t\tqueryClient.setQueryData([\"proxy-host\", values.id], (old: ProxyHost) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"proxy-host\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: ProxyHost) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"proxy-host\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"proxy-hosts\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"host-report\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\t},\n\t});\n};\n\nexport { useProxyHost, useSetProxyHost };\n"
  },
  {
    "path": "frontend/src/hooks/useProxyHosts.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getProxyHosts, type ProxyHost, type ProxyHostExpansion } from \"src/api/backend\";\n\nconst fetchProxyHosts = (expand?: ProxyHostExpansion[]) => {\n\treturn getProxyHosts(expand);\n};\n\nconst useProxyHosts = (expand?: ProxyHostExpansion[], options = {}) => {\n\treturn useQuery<ProxyHost[], Error>({\n\t\tqueryKey: [\"proxy-hosts\", { expand }],\n\t\tqueryFn: () => fetchProxyHosts(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchProxyHosts, useProxyHosts };\n"
  },
  {
    "path": "frontend/src/hooks/useRedirectionHost.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n\tcreateRedirectionHost,\n\tgetRedirectionHost,\n\ttype RedirectionHost,\n\tupdateRedirectionHost,\n} from \"src/api/backend\";\n\nconst fetchRedirectionHost = (id: number | \"new\") => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\townerUserId: 0,\n\t\t\tdomainNames: [],\n\t\t\tforwardDomainName: \"\",\n\t\t\tpreservePath: false,\n\t\t\tcertificateId: 0,\n\t\t\tsslForced: false,\n\t\t\tadvancedConfig: \"\",\n\t\t\tmeta: {},\n\t\t\thttp2Support: false,\n\t\t\tforwardScheme: \"auto\",\n\t\t\tforwardHttpCode: 301,\n\t\t\tblockExploits: false,\n\t\t\tenabled: true,\n\t\t\thstsEnabled: false,\n\t\t\thstsSubdomains: false,\n\t\t} as RedirectionHost);\n\t}\n\treturn getRedirectionHost(id, [\"owner\"]);\n};\n\nconst useRedirectionHost = (id: number | \"new\", options = {}) => {\n\treturn useQuery<RedirectionHost, Error>({\n\t\tqueryKey: [\"redirection-host\", id],\n\t\tqueryFn: () => fetchRedirectionHost(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetRedirectionHost = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: RedirectionHost) =>\n\t\t\tvalues.id ? updateRedirectionHost(values) : createRedirectionHost(values),\n\t\tonMutate: (values: RedirectionHost) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"redirection-host\", values.id]);\n\t\t\tqueryClient.setQueryData([\"redirection-host\", values.id], (old: RedirectionHost) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"redirection-host\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: RedirectionHost) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"redirection-host\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"redirection-hosts\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"host-report\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\t},\n\t});\n};\n\nexport { useRedirectionHost, useSetRedirectionHost };\n"
  },
  {
    "path": "frontend/src/hooks/useRedirectionHosts.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getRedirectionHosts, type HostExpansion, type RedirectionHost } from \"src/api/backend\";\n\nconst fetchRedirectionHosts = (expand?: HostExpansion[]) => {\n\treturn getRedirectionHosts(expand);\n};\n\nconst useRedirectionHosts = (expand?: HostExpansion[], options = {}) => {\n\treturn useQuery<RedirectionHost[], Error>({\n\t\tqueryKey: [\"redirection-hosts\", { expand }],\n\t\tqueryFn: () => fetchRedirectionHosts(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchRedirectionHosts, useRedirectionHosts };\n"
  },
  {
    "path": "frontend/src/hooks/useSetting.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { getSetting, type Setting, updateSetting } from \"src/api/backend\";\n\nconst fetchSetting = (id: string) => {\n\treturn getSetting(id);\n};\n\nconst useSetting = (id: string, options = {}) => {\n\treturn useQuery<Setting, Error>({\n\t\tqueryKey: [\"setting\", id],\n\t\tqueryFn: () => fetchSetting(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetSetting = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: Setting) => updateSetting(values),\n\t\tonMutate: (values: Setting) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"setting\", values.id]);\n\t\t\tqueryClient.setQueryData([\"setting\", values.id], (old: Setting) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"setting\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: Setting) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"setting\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t},\n\t});\n};\n\nexport { useSetting, useSetSetting };\n"
  },
  {
    "path": "frontend/src/hooks/useStream.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createStream, getStream, type Stream, updateStream } from \"src/api/backend\";\n\nconst fetchStream = (id: number | \"new\") => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\townerUserId: 0,\n\t\t\ttcpForwarding: true,\n\t\t\tudpForwarding: false,\n\t\t\tmeta: {},\n\t\t\tenabled: true,\n\t\t\tcertificateId: 0,\n\t\t} as Stream);\n\t}\n\treturn getStream(id, [\"owner\"]);\n};\n\nconst useStream = (id: number | \"new\", options = {}) => {\n\treturn useQuery<Stream, Error>({\n\t\tqueryKey: [\"stream\", id],\n\t\tqueryFn: () => fetchStream(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetStream = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: Stream) => (values.id ? updateStream(values) : createStream(values)),\n\t\tonMutate: (values: Stream) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"stream\", values.id]);\n\t\t\tqueryClient.setQueryData([\"stream\", values.id], (old: Stream) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"stream\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: Stream) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"stream\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"streams\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"host-report\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\t},\n\t});\n};\n\nexport { useStream, useSetStream };\n"
  },
  {
    "path": "frontend/src/hooks/useStreams.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getStreams, type HostExpansion, type Stream } from \"src/api/backend\";\n\nconst fetchStreams = (expand?: HostExpansion[]) => {\n\treturn getStreams(expand);\n};\n\nconst useStreams = (expand?: HostExpansion[], options = {}) => {\n\treturn useQuery<Stream[], Error>({\n\t\tqueryKey: [\"streams\", { expand }],\n\t\tqueryFn: () => fetchStreams(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchStreams, useStreams };\n"
  },
  {
    "path": "frontend/src/hooks/useTheme.ts",
    "content": "import { Dark, Light, useTheme as useThemeContext } from \"src/context\";\n\n// Simple hook wrapper for clarity and scalability\nconst useTheme = () => {\n\treturn useThemeContext();\n};\n\nexport { useTheme, Dark, Light };\n"
  },
  {
    "path": "frontend/src/hooks/useUser.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createUser, getUser, type User, updateUser } from \"src/api/backend\";\n\nconst fetchUser = (id: number | string) => {\n\tif (id === \"new\") {\n\t\treturn Promise.resolve({\n\t\t\tid: 0,\n\t\t\tcreatedOn: \"\",\n\t\t\tmodifiedOn: \"\",\n\t\t\tisDisabled: false,\n\t\t\temail: \"\",\n\t\t\tname: \"\",\n\t\t\tnickname: \"\",\n\t\t\troles: [],\n\t\t\tavatar: \"\",\n\t\t} as User);\n\t}\n\treturn getUser(id, [\"permissions\"]);\n};\n\nconst useUser = (id: string | number, options = {}) => {\n\treturn useQuery<User, Error>({\n\t\tqueryKey: [\"user\", id],\n\t\tqueryFn: () => fetchUser(id),\n\t\tstaleTime: 60 * 1000, // 1 minute\n\t\t...options,\n\t});\n};\n\nconst useSetUser = () => {\n\tconst queryClient = useQueryClient();\n\treturn useMutation({\n\t\tmutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)),\n\t\tonMutate: (values: User) => {\n\t\t\tif (!values.id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst previousObject = queryClient.getQueryData([\"user\", values.id]);\n\t\t\tqueryClient.setQueryData([\"user\", values.id], (old: User) => ({\n\t\t\t\t...old,\n\t\t\t\t...values,\n\t\t\t}));\n\t\t\treturn () => queryClient.setQueryData([\"user\", values.id], previousObject);\n\t\t},\n\t\tonError: (_, __, rollback: any) => rollback(),\n\t\tonSuccess: async ({ id }: User) => {\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"user\", id] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"users\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"audit-logs\"] });\n\t\t},\n\t});\n};\n\nexport { useUser, useSetUser };\n"
  },
  {
    "path": "frontend/src/hooks/useUsers.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { getUsers, type User, type UserExpansion } from \"src/api/backend\";\n\nconst fetchUsers = (expand?: UserExpansion[]) => {\n\treturn getUsers(expand);\n};\n\nconst useUsers = (expand?: UserExpansion[], options = {}) => {\n\treturn useQuery<User[], Error>({\n\t\tqueryKey: [\"users\", { expand }],\n\t\tqueryFn: () => fetchUsers(expand),\n\t\tstaleTime: 60 * 1000,\n\t\t...options,\n\t});\n};\n\nexport { fetchUsers, useUsers };\n"
  },
  {
    "path": "frontend/src/locale/IntlProvider.tsx",
    "content": "import { createIntl, createIntlCache } from \"react-intl\";\nimport langBg from \"./lang/bg.json\";\nimport langDe from \"./lang/de.json\";\nimport langPt from \"./lang/pt.json\";\nimport langEn from \"./lang/en.json\";\nimport langEs from \"./lang/es.json\";\nimport langEt from \"./lang/et.json\";\nimport langFr from \"./lang/fr.json\";\nimport langGa from \"./lang/ga.json\";\nimport langId from \"./lang/id.json\";\nimport langIt from \"./lang/it.json\";\nimport langJa from \"./lang/ja.json\";\nimport langKo from \"./lang/ko.json\";\nimport langNl from \"./lang/nl.json\";\nimport langPl from \"./lang/pl.json\";\nimport langRu from \"./lang/ru.json\";\nimport langSk from \"./lang/sk.json\";\nimport langCs from \"./lang/cs.json\";\nimport langVi from \"./lang/vi.json\";\nimport langZh from \"./lang/zh.json\";\nimport langTr from \"./lang/tr.json\";\nimport langHu from \"./lang/hu.json\";\nimport langNo from \"./lang/no.json\";\nimport langList from \"./lang/lang-list.json\";\n\n// first item of each array should be the language code,\n// not the country code\n// Remember when adding to this list, also update check-locales.js script\nconst localeOptions = [\n  [\"en\", \"en-US\", langEn],\n  [\"de\", \"de-DE\", langDe],\n  [\"es\", \"es-ES\", langEs],\n  [\"et\", \"et-EE\", langEt],\n  [\"pt\", \"pt-PT\", langPt],\n  [\"fr\", \"fr-FR\", langFr],\n  [\"ga\", \"ga-IE\", langGa],\n  [\"ja\", \"ja-JP\", langJa],\n  [\"it\", \"it-IT\", langIt],\n  [\"nl\", \"nl-NL\", langNl],\n  [\"pl\", \"pl-PL\", langPl],\n  [\"ru\", \"ru-RU\", langRu],\n  [\"sk\", \"sk-SK\", langSk],\n  [\"cs\", \"cs-CZ\", langCs],\n  [\"vi\", \"vi-VN\", langVi],\n  [\"zh\", \"zh-CN\", langZh],\n  [\"ko\", \"ko-KR\", langKo],\n  [\"bg\", \"bg-BG\", langBg],\n  [\"id\", \"id-ID\", langId],\n  [\"tr\", \"tr-TR\", langTr],\n  [\"hu\", \"hu-HU\", langHu],\n  [\"no\", \"no-NO\", langNo],\n];\n\nconst loadMessages = (locale?: string): typeof langList & typeof langEn => {\n  const thisLocale = (locale || \"en\").slice(0, 2);\n\n  // ensure this lang exists in localeOptions above, otherwise fallback to en\n  if (thisLocale === \"en\" || !localeOptions.some(([code]) => code === thisLocale)) {\n    return Object.assign({}, langList, langEn);\n  }\n\n  return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);\n};\n\nconst getFlagCodeForLocale = (locale?: string) => {\n  const thisLocale = (locale || \"en\").slice(0, 2);\n\n  // only add to this if your flag is different from the locale code\n  const specialCases: Record<string, string> = {\n    ja: \"jp\", // Japan\n    zh: \"cn\", // China\n    vi: \"vn\", // Vietnam\n    ko: \"kr\", // Korea\n    cs: \"cz\", // Czechia\n  };\n\n  if (specialCases[thisLocale]) {\n    return specialCases[thisLocale].toUpperCase();\n  }\n  return thisLocale.toUpperCase();\n};\n\nconst getLocale = (short = false) => {\n  let loc = window.localStorage.getItem(\"locale\");\n  if (!loc) {\n    loc = document.documentElement.lang;\n  }\n  if (short) {\n    return loc.slice(0, 2);\n  }\n  // finally, fallback\n  if (!loc) {\n    loc = \"en\";\n  }\n  return loc;\n};\n\nconst cache = createIntlCache();\n\nconst initialMessages = loadMessages(getLocale());\nlet intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);\n\nconst changeLocale = (locale: string): void => {\n  const messages = loadMessages(locale);\n  intl = createIntl({ locale, messages }, cache);\n  window.localStorage.setItem(\"locale\", locale);\n  document.documentElement.lang = locale;\n};\n\n// This is a translation component that wraps the translation in a span with a data\n// attribute so devs can inspect the element to see the translation ID\nconst T = ({\n  id,\n  data,\n  tData,\n}: {\n  id: string;\n  data?: Record<string, string | number | undefined>;\n  tData?: Record<string, string>;\n}) => {\n  const translatedData: Record<string, string> = {};\n  if (tData) {\n    // iterate over tData and translate each value\n    Object.entries(tData).forEach(([key, value]) => {\n      translatedData[key] = intl.formatMessage({ id: value });\n    });\n  }\n  return (\n    <span data-translation-id={id}>\n      {intl.formatMessage(\n        { id },\n        {\n          ...data,\n          ...translatedData,\n        },\n      )}\n    </span>\n  );\n};\n\n//console.log(\"L:\", localeOptions);\n\nexport { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };\n"
  },
  {
    "path": "frontend/src/locale/README.md",
    "content": "# Internationalisation support\n\n## Before you start\n\nIt's highly recommended that you spin up a development instance of this project\non your docker capable server. It's pretty easy:\n\n```bash\ngit clone https://github.com/NginxProxyManager/nginx-proxy-manager.git\ncd nginx-proxy-manager\n./scripts/start-dev -f\n```\n\nThen after a while, you can access http://yourserverip:3081\n\nThis stack will watch the file system for changes, especially to language files,\nand reload the site you have open in the browser.\n\n\n## Adding new translations\n\nModify the files in the `src` folder. Follow the conventions already there.\n\nWhen the development stack is running, it will sort the locale lang files\nfor you when you save.\n\n\n## After making changes\n\nIf you're NOT running the development stack, you will need to run\n`yarn locale-compile` in the `frontend` folder for\nthe new translations to be compiled into the `lang` folder.\n\n\n## Adding a whole new language\n\nThere's a fair bit you'll need to touch. Here's a list that may\nnot be complete by the time you're reading this:\n\n- frontend/src/locale/src/[yourlang].json\n- frontend/src/locale/src/lang-list.json\n- frontend/src/locale/src/HelpDoc/[yourlang]/*\n- frontend/src/locale/src/HelpDoc/index.tsx\n- frontend/src/locale/IntlProvider.tsx\n- frontend/check-locales.cjs\n\n\n## Checking for missing translations in languages\n\nRun `node check-locales.cjs` in this frontend folder.\n"
  },
  {
    "path": "frontend/src/locale/Utils.test.tsx",
    "content": "import { formatDateTime } from \"src/locale\";\nimport { afterAll, beforeAll, describe, expect, it } from \"vitest\";\n\ndescribe(\"DateFormatter\", () => {\n\t// Keep a reference to the real Intl to restore later\n\tconst RealIntl = global.Intl;\n\tconst desiredTimeZone = \"Europe/London\";\n\tconst desiredLocale = \"en-GB\";\n\n\tbeforeAll(() => {\n\t\t// Ensure Node-based libs using TZ behave deterministically\n\t\ttry {\n\t\t\tprocess.env.TZ = desiredTimeZone;\n\t\t} catch {\n\t\t\t// ignore if not available\n\t\t}\n\n\t\t// Mock Intl.DateTimeFormat so formatting is stable regardless of host\n\t\tconst MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {\n\t\t\tconstructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) {\n\t\t\t\tsuper(desiredLocale, {\n\t\t\t\t\t...options,\n\t\t\t\t\ttimeZone: desiredTimeZone,\n\t\t\t\t});\n\t\t\t}\n\t\t} as unknown as typeof Intl.DateTimeFormat;\n\n\t\tglobal.Intl = {\n\t\t\t...RealIntl,\n\t\t\tDateTimeFormat: MockedDateTimeFormat,\n\t\t};\n\t});\n\n\tafterAll(() => {\n\t\t// Restore original Intl after tests\n\t\tglobal.Intl = RealIntl;\n\t});\n\n\tit(\"format date from iso date\", () => {\n\t\tconst value = \"2024-01-01T00:00:00.000Z\";\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"1 Jan 2024, 12:00:00 am\");\n\t});\n\n\tit(\"format date from unix timestamp number\", () => {\n\t\tconst value = 1762476112;\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"7 Nov 2025, 12:41:52 am\");\n\t});\n\n\tit(\"format date from unix timestamp string\", () => {\n\t\tconst value = \"1762476112\";\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"7 Nov 2025, 12:41:52 am\");\n\t});\n\n\tit(\"catch bad format from string\", () => {\n\t\tconst value = \"this is not a good date\";\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"this is not a good date\");\n\t});\n\n\tit(\"catch bad format from number\", () => {\n\t\tconst value = -100;\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"-100\");\n\t});\n\n\tit(\"catch bad format from number as string\", () => {\n\t\tconst value = \"-100\";\n\t\tconst text = formatDateTime(value);\n\t\texpect(text).toBe(\"-100\");\n\t});\n});\n"
  },
  {
    "path": "frontend/src/locale/Utils.ts",
    "content": "import {\n\tfromUnixTime,\n\ttype IntlFormatFormatOptions,\n\tintlFormat,\n\tparseISO,\n} from \"date-fns\";\n\nconst isUnixTimestamp = (value: unknown): boolean => {\n\tif (typeof value !== \"number\" && typeof value !== \"string\") return false;\n\tconst num = Number(value);\n\tif (!Number.isFinite(num)) return false;\n\t// Check plausible Unix timestamp range: from 1970 to ~year 3000\n\t// Support both seconds and milliseconds\n\tif (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)\n\tif (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)\n\treturn false;\n};\n\nconst parseDate = (value: string | number): Date | null => {\n\tif (typeof value !== \"number\" && typeof value !== \"string\") return null;\n\ttry {\n\t\treturn isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst formatDateTime = (value: string | number, locale = \"en-US\"): string => {\n\tconst d = parseDate(value);\n\tif (!d) return `${value}`;\n\ttry {\n\t\treturn intlFormat(\n\t\t\td,\n\t\t\t{\n\t\t\t\tdateStyle: \"medium\",\n\t\t\t\ttimeStyle: \"medium\",\n\t\t\t\thourCycle: \"h12\",\n\t\t\t} as IntlFormatFormatOptions,\n\t\t\t{ locale },\n\t\t);\n\t} catch {\n\t\treturn `${value}`;\n\t}\n};\n\nexport { formatDateTime, parseDate, isUnixTimestamp };\n"
  },
  {
    "path": "frontend/src/locale/index.ts",
    "content": "export * from \"./IntlProvider\";\nexport * from \"./Utils\";\n"
  },
  {
    "path": "frontend/src/locale/scripts/locale-sort.cjs",
    "content": "#!/usr/bin/env node\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst DIR = path.resolve(__dirname, \"../src\");\n\n// Function to sort object keys recursively\nfunction sortKeys(obj) {\n\tif (obj === null || typeof obj !== \"object\" || obj instanceof Array) {\n\t\treturn obj;\n\t}\n\n\tconst sorted = {};\n\tconst keys = Object.keys(obj).sort();\n\tfor (const key of keys) {\n\t\tconst value = obj[key];\n\t\tif (typeof value === \"object\" && value !== null && !(value instanceof Array)) {\n\t\t\tsorted[key] = sortKeys(value);\n\t\t} else {\n\t\t\tsorted[key] = value;\n\t\t}\n\t}\n\treturn sorted;\n}\n\n// Get all JSON files in the directory\nconst files = fs.readdirSync(DIR).filter((file) => {\n\treturn file.endsWith(\".json\") && file !== \"lang-list.json\";\n});\n\nfiles.forEach((file) => {\n\tconst filePath = path.join(DIR, file);\n\tconst stats = fs.statSync(filePath);\n\n\tif (!stats.isFile()) {\n\t\treturn;\n\t}\n\n\tif (stats.size === 0) {\n\t\tconsole.log(`Skipping empty file ${file}`);\n\t\treturn;\n\t}\n\n\ttry {\n\t\t// Read original content\n\t\tconst originalContent = fs.readFileSync(filePath, \"utf8\");\n\t\tconst originalJson = JSON.parse(originalContent);\n\n\t\t// Sort keys\n\t\tconst sortedJson = sortKeys(originalJson);\n\n\t\t// Convert back to string with tabs\n\t\tconst sortedContent = JSON.stringify(sortedJson, null, \"\\t\") + \"\\n\";\n\n\t\t// Compare (normalize whitespace)\n\t\tif (originalContent.trim() === sortedContent.trim()) {\n\t\t\tconsole.log(`${file} is already sorted`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Write sorted content\n\t\tfs.writeFileSync(filePath, sortedContent, \"utf8\");\n\t\tconsole.log(`Sorted ${file}`);\n\t} catch (error) {\n\t\tconsole.error(`Error processing ${file}:`, error.message);\n\t}\n});\n\n"
  },
  {
    "path": "frontend/src/locale/scripts/locale-sort.sh",
    "content": "#!/bin/bash\nset -e -o pipefail\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$DIR/../src\" || exit 1\n\nif ! command -v jq &> /dev/null; then\n\techo \"jq could not be found, please install it to sort JSON files.\"\n\texit 1\nfi\n\n# iterate over all json files in the current directory\nfor file in *.json; do\n\tif [[ -f \"$file\" ]]; then\n\t\tif [[ ! -s \"$file\" ]]; then\n\t\t\techo \"Skipping empty file $file\"\n\t\t\tcontinue\n\t\tfi\n\n\t\tif [ \"$file\" == \"lang-list.json\" ]; then\n\t\t\tcontinue\n\t\tfi\n\n\t\t# get content of file before sorting\n\t\toriginal_content=$(<\"$file\")\n\t\t# compare with sorted content\n\t\tsorted_content=$(jq --tab --sort-keys . \"$file\")\n\t\tif [ \"$original_content\" == \"$sorted_content\" ]; then\n\t\t\techo \"$file is already sorted\"\n\t\t\tcontinue\n\t\tfi\n\n\t\techo \"Sorting $file\"\n\t\ttmp=$(mktemp) && jq --tab --sort-keys . \"$file\" > \"$tmp\" && mv \"$tmp\" \"$file\"\n\tfi\ndone\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/AccessLists.md",
    "content": "## Какво представлява Списъкът за достъп?\n\nСписъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация.\n\nМожете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_.\n\nТова е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/Certificates.md",
    "content": "## Помощ за сертификати\n\n### HTTP сертификат\n\nHTTP валидираният сертификат означава, че сървърите на Let’s Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата.\n\nЗа този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява.\n\nТози процес _не_ поддържа wildcard домейни.\n\n### DNS сертификат\n\nDNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Let’s Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата.\n\nНе е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP.\n\nТози процес _поддържа_ wildcard домейни.\n\n### Персонализиран сертификат\n\nИзползвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/DeadHosts.md",
    "content": "## Какво представлява 404 хост?\n\n404 хост е просто конфигурация на хост, който показва страница с грешка 404.\n\nТова може да е полезно, когато вашият домейн е индексиран в търсачките и искате\nда предоставите по-приятна страница за грешка или да уведомите индексиращите системи,\nче страниците на домейна вече не съществуват.\n\nДопълнително предимство на този хост е възможността да проследявате логовете на заявките\nкъм него и да виждате реферерите.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md",
    "content": "## Какво представлява Прокси хост?\n\nПрокси хост е входна точка за уеб услуга, която искате да препращате.\n\nТой предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.\n\nПрокси хостовете са най-често използваната функция в Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md",
    "content": "## Какво представлява Хост за пренасочване?\n\nХостът за пренасочване пренасочва заявките от входящия домейн и прехвърля\nпотребителя към друг домейн.\n\nНай-честата причина за използване на този тип хост е, когато вашият уебсайт\nпромени домейна си, но все още има линкове от търсачки или реферери, които сочат към стария домейн.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/Streams.md",
    "content": "## Какво представлява Потокът (Stream)?\n\nОтносително нова функция за Nginx, Потокът позволява препращане на TCP/UDP\nтрафик директно към друг компютър в мрежата.\n\nТова е полезно, ако хоствате игрови сървъри, FTP или SSH сървъри.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/bg/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/AccessLists.md",
    "content": "## Co je seznam přístupů?\n\nSeznamy přístupů poskytují blacklist nebo whitelist konkrétních IP adres klientů spolu s ověřením pro proxy hostitele prostřednictvím základního ověřování HTTP.\n\nMůžete nakonfigurovat více pravidel pro klienty, uživatelská jména a hesla pro jeden seznam přístupu a poté ho použít na jednoho nebo více proxy hostitelů.\n\nToto je nejužitečnější pro přesměrované webové služby, které nemají vestavěné ověřovací mechanismy, nebo pokud se chcete chránit před neznámými klienty.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/Certificates.md",
    "content": "## Pomoc s certifikáty\n\n### Certifikát HTTP\n\nCertifikát ověřený prostřednictvím protokolu HTTP znamená, že servery Let's Encrypt se\npokusí připojit k vašim doménám přes protokol HTTP (nikoli HTTPS!) a v případě úspěchu\nvydají váš certifikát.\n\nPro tuto metodu budete muset mít pro své domény vytvořeného _Proxy Host_, který\nje přístupný přes HTTP a směruje na tuto instalaci Nginx. Po vydání certifikátu\nmůžete změnit _Proxy Host_ tak, aby tento certifikát používal i pro HTTPS\npřipojení. _Proxy Host_ však bude stále potřeba nakonfigurovat pro přístup přes HTTP,\naby se certifikát mohl obnovit.\n\nTento proces _nepodporuje_ domény se zástupnými znaky.\n\n### Certifikát DNS\n\nCertifikát ověřený DNS vyžaduje použití pluginu DNS Provider. Tento DNS\nProvider se použije na vytvoření dočasných záznamů ve vaší doméně a poté Let's\nEncrypt ověří tyto záznamy, aby se ujistil, že jste vlastníkem, a pokud bude úspěšný,\nvydá váš certifikát.\n\nPřed požádáním o tento typ certifikátu není potřeba vytvořit _Proxy Host_.\nNení také potřeba mít _Proxy Host_ nakonfigurovaný pro přístup HTTP.\n\nTento proces _podporuje_ domény se zástupnými znaky.\n\n### Vlastní certifikát\n\nTuto možnost použijte na nahrání vlastního SSL certifikátu, který vám poskytla vaše\ncertifikační autorita.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/DeadHosts.md",
    "content": "## Co je to 404 Host?\n\n404 Host je jednoduše nastavení hostitele, které zobrazuje stránku 404.\n\nTo může být užitečné, pokud je vaše doména uvedena ve vyhledávačích a chcete\nposkytnout hezčí chybovou stránku nebo konkrétně oznámit vyhledávačům, že\nstránky domény již neexistují.\n\nDalší výhodou tohoto hostitele je sledování protokolů o návštěvách a\nzobrazení odkazů.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/ProxyHosts.md",
    "content": "## Co je proxy hostitel?\n\nProxy hostitel je příchozí koncový bod pro webovou službu, kterou chcete přesměrovat.\n\nPoskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL.\n\nProxy hostitelé jsou nejběžnějším použitím pro Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/RedirectionHosts.md",
    "content": "## Co je přesměrovací hostitel?\n\nPřesměrovací hostitel přesměruje požadavky z příchozí domény a přesměruje\nnávštěvníka na jinou doménu.\n\nNejčastějším důvodem pro použití tohoto typu hostitele je situace, kdy vaše webová stránka změní\ndoménu, ale stále máte odkazy ve vyhledávačích nebo referenční odkazy směřující na starou doménu.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/Streams.md",
    "content": "## Co je stream?\n\nStream je relativně nová funkce pro Nginx, která slouží na přesměrování TCP/UDP\ndatového toku přímo do jiného počítače v síti.\n\nPokud provozujete herní servery, FTP nebo SSH servery, tato funkce se vám může hodit.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/cs/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/AccessLists.md",
    "content": "## Was ist eine Zugriffsliste?\n\nZugriffslisten bieten eine Blacklist oder Whitelist mit bestimmten Client-IP-Adressen sowie eine Authentifizierung für die Proxy-Hosts über die grundlegende HTTP-Authentifizierung.\n\nSie können mehrere Client-Regeln, Benutzernamen und Passwörter für eine einzelne Zugriffsliste konfigurieren und diese dann auf einen oder mehrere Proxy-Hosts anwenden.\n\nDies ist besonders nützlich für weitergeleitete Webdienste, die keine integrierten Authentifizierungsmechanismen haben, oder wenn Sie sich vor unbekannten Clients schützen möchten.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/Certificates.md",
    "content": "## Hilfe zu Zertifikaten\n\n### HTTP-Zertifikat\n\nEin HTTP-validiertes Zertifikat bedeutet, dass Let's Encrypt-Server\nversuchen, Ihre Domains über HTTP (nicht HTTPS!) zu erreichen, und wenn dies erfolgreich ist,\nstellen sie Ihr Zertifikat aus.\n\nFür diese Methode müssen Sie einen _Proxy-Host_ für Ihre Domain(s) erstellen, der\nüber HTTP zugänglich ist und auf diese Nginx-Installation verweist. Nachdem ein Zertifikat\nausgestellt wurde, können Sie den _Proxy-Host_ so ändern, dass dieses Zertifikat auch für HTTPS-Verbindungen\nverwendet wird. Der _Proxy-Host_ muss jedoch weiterhin für den HTTP-Zugriff konfiguriert sein,\n damit das Zertifikat erneuert werden kann.\n\nDieser Prozess unterstützt keine Wildcard-Domains.\n\n### DNS-Zertifikat\n\nFür ein DNS-validiertes Zertifikat müssen Sie ein DNS-Provider-Plugin verwenden. Dieser DNS-\nProvider wird verwendet, um temporäre Einträge auf Ihrer Domain zu erstellen. Anschließend fragt Let's\nEncrypt diese Einträge ab, um sicherzustellen, dass Sie der Eigentümer sind. Bei Erfolg wird\nIhr Zertifikat ausgestellt.\n\nSie müssen vor der Beantragung dieser Art von Zertifikat keinen _Proxy-Host_ erstellen.\nSie müssen Ihren _Proxy-Host_ auch nicht für den HTTP-Zugriff konfigurieren.\n\nDieser Prozess unterstützt Wildcard-Domains.\n\n### Benutzerdefiniertes Zertifikat\n\nVerwenden Sie diese Option, um Ihr eigenes SSL-Zertifikat hochzuladen, das Ihnen von Ihrer eigenen\nZertifizierungsstelle bereitgestellt wurde.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/DeadHosts.md",
    "content": "## Was ist ein 404-Host?\n\nEin 404-Host ist ein Host-Setup, das eine 404-Seite anzeigt.\n\nDies kann nützlich sein, wenn Ihre Domain in Suchmaschinen gelistet ist und Sie\neine ansprechendere Fehlerseite bereitstellen oder den Suchindexern ausdrücklich mitteilen möchten, dass\ndie Domain-Seiten nicht mehr existieren.\n\nEin weiterer Vorteil dieses Hosts besteht darin, dass Sie die Protokolle für Zugriffe darauf verfolgen und\ndie Verweise anzeigen können.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/ProxyHosts.md",
    "content": "## Was ist ein Proxy-Host?\n\nEin Proxy-Host ist der eingehende Endpunkt für einen Webdienst, den Sie weiterleiten möchten.\n\nEr bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat.\n\nProxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/RedirectionHosts.md",
    "content": "## Was ist ein Redirection Host?\n\nEin Redirection Host leitet Anfragen von der eingehenden Domain weiter und leitet den\nBesucher zu einer anderen Domain weiter.\n\nDer häufigste Grund für die Verwendung dieses Host-Typs ist, wenn Ihre Website die\nDomain wechselt, aber Sie noch Suchmaschinen- oder Referrer-Links haben, die auf die alte Domain verweisen."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/Streams.md",
    "content": "## Was ist ein Stream?\n\nEin Stream ist eine relativ neue Funktion von Nginx, die dazu dient, TCP/UDP-Datenverkehr\ndirekt an einen anderen Computer im Netzwerk weiterzuleiten.\n\nWenn Sie Spielserver, FTP- oder SSH-Server betreiben, kann dies sehr nützlich sein."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/de/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/AccessLists.md",
    "content": "## What is an Access List?\n\nAccess Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\n\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to one or more _Proxy Hosts_.\n\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or when you want to protect from unknown clients.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/Certificates.md",
    "content": "## Certificates Help\n\n### HTTP Certificate\n\nA HTTP validated certificate means Let's Encrypt servers will\nattempt to reach your domains over HTTP (not HTTPS!) and if successful, they\nwill issue your certificate.\n\nFor this method, you will have to have a _Proxy Host_ created for your domains(s) that\nis accessible with HTTP and pointing to this Nginx installation. After a certificate\nhas been given, you can modify the _Proxy Host_ to also use this certificate for HTTPS\nconnections. However, the _Proxy Host_ will still need to be configured for HTTP access\nin order for the certificate to renew.\n\nThis process _does not_ support wildcard domains.\n\n### DNS Certificate\n\nA DNS validated certificate requires you to use a DNS Provider plugin. This DNS\nProvider will be used to create temporary records on your domain and then Let's\nEncrypt will query those records to be sure you're the owner and if successful, they\nwill issue your certificate.\n\nYou do not need a _Proxy Host_ to be created prior to requesting this type of\ncertificate. Nor do you need to have your _Proxy Host_ configured for HTTP access.\n\nThis process _does_ support wildcard domains.\n\n### Custom Certificate\n\nUse this option to upload your own SSL Certificate, as provided by your own\nCertificate Authority.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/DeadHosts.md",
    "content": "## What is a 404 Host?\n\nA 404 Host is simply a host setup that shows a 404 page.\n\nThis can be useful when your domain is listed in search engines and you want\nto provide a nicer error page or specifically to tell the search indexers that\nthe domain pages no longer exist.\n\nAnother benefit of having this host is to track the logs for hits to it and\nview the referrers.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/ProxyHosts.md",
    "content": "## What is a Proxy Host?\n\nA Proxy Host is the incoming endpoint for a web service that you want to forward.\n\nIt provides optional SSL termination for your service that might not have SSL support built in.\n\nProxy Hosts are the most common use for the Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md",
    "content": "## What is a Redirection Host?\n\nA Redirection Host will redirect requests from the incoming domain and push the\nviewer to another domain.\n\nThe most common reason to use this type of host is when your website changes\ndomains but you still have search engine or referrer links pointing to the old domain.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/Streams.md",
    "content": "## What is a Stream?\n\nA relatively new feature for Nginx, a Stream will serve to forward TCP/UDP\ntraffic directly to another computer on the network.\n\nIf you're running game servers, FTP or SSH servers this can come in handy.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/en/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/AccessLists.md",
    "content": "## ¿Qué es una Lista de Acceso?\n\nLas Listas de Acceso proporcionan una lista negra o blanca de direcciones IP de cliente específicas junto con autenticación para los Hosts Proxy a través de Autenticación HTTP Básica.\n\nPuede configurar múltiples reglas de cliente, nombres de usuario y contraseñas para una única Lista de Acceso y luego aplicarla a uno o más _Hosts Proxy_.\n\nEsto es más útil para servicios web reenviados que no tienen mecanismos de autenticación integrados o cuando desea protegerse de clientes desconocidos.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/Certificates.md",
    "content": "## Ayuda de Certificados\n\n### Certificado HTTP\n\nUn certificado validado por HTTP significa que los servidores de Let's Encrypt\nintentarán acceder a tus dominios a través de HTTP (¡no HTTPS!) y, si tienen éxito,\nemitirán tu certificado.\n\nPara este método, deberás tener un _Host Proxy_ creado para tu(s) dominio(s) que\nsea accesible por HTTP y que apunte a esta instalación de Nginx. Después de que se\nhaya emitido un certificado, puedes modificar el _Host Proxy_ para que también use\neste certificado para conexiones HTTPS. Sin embargo, el _Host Proxy_ seguirá\nnecesitando estar configurado para acceso HTTP para que el certificado se renueve.\n\nEste proceso _no_ admite dominios comodín.\n\n### Certificado DNS\n\nUn certificado validado por DNS requiere que uses un complemento de Proveedor de DNS.\nEste Proveedor de DNS se usará para crear registros temporales en tu dominio y luego\nLet's Encrypt consultará esos registros para asegurarse de que eres el propietario y,\nsi tiene éxito, emitirá tu certificado.\n\nNo necesitas tener un _Host Proxy_ creado antes de solicitar este tipo de certificado.\nTampoco necesitas tener tu _Host Proxy_ configurado para acceso HTTP.\n\nEste proceso _sí_ admite dominios comodín.\n\n### Certificado Personalizado\n\nUsa esta opción para cargar tu propio Certificado SSL, proporcionado por tu propia\nAutoridad de Certificación.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/DeadHosts.md",
    "content": "## ¿Qué es un Host 404?\n\nUn Host 404 es simplemente una configuración de host que muestra una página 404.\n\nEsto puede ser útil cuando tu dominio está listado en los motores de búsqueda y deseas\nproporcionar una página de error más agradable o específicamente para indicar a los indexadores de búsqueda que\nlas páginas del dominio ya no existen.\n\nOtro beneficio de tener este host es rastrear los registros de visitas a él y\nver los referentes.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/ProxyHosts.md",
    "content": "## ¿Qué es un Host Proxy?\n\nUn Host Proxy es el punto de entrada para un servicio web que deseas reenviar.\n\nProporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado.\n\nLos Hosts Proxy son el uso más común del Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/RedirectionHosts.md",
    "content": "## ¿Qué es un Host de Redirección?\n\nUn Host de Redirección redirigirá las solicitudes del dominio entrante e impulsará al\nvisitante a otro dominio.\n\nLa razón más común para usar este tipo de host es cuando tu sitio web cambia de\ndominios pero aún tienes enlaces de motores de búsqueda o referencias apuntando al dominio anterior.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/Streams.md",
    "content": "## ¿Qué es un Stream?\n\nUna característica relativamente nueva para Nginx, un Stream servirá para reenviar tráfico TCP/UDP\ndirectamente a otra computadora en la red.\n\nSi estás ejecutando servidores de juegos, FTP o servidores SSH esto puede ser muy útil.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/es/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/AccessLists.md",
    "content": "## Mis on juurdepääsuloend?\n\nLigipääsuloendid pakuvad konkreetsete klientide IP-aadresside musta või valget nimekirja koos puhverserverite autentimisega põhilise HTTP-autentimise kaudu.\n\nSaate ühe juurdepääsuloendi jaoks konfigureerida mitu kliendireeglit, kasutajanime ja parooli ning seejärel rakendada neid ühele või mitmele _puhverserverile_.\n\nSee on kõige kasulikum edastatud veebiteenuste puhul, millel pole sisseehitatud autentimismehhanisme või kui soovite kaitsta tundmatute klientide eest."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/Certificates.md",
    "content": "## Sertifikaatide abi\n\n### HTTP-sertifikaat\n\nHTTP-valideeritud sertifikaat tähendab, et Let's Encrypti serverid\n\nproovivad teie domeenidega ühendust luua HTTP (mitte HTTPS!) kaudu ja kui see õnnestub,\nväljastavad nad teile sertifikaadi.\n\nSelle meetodi jaoks peate oma domeeni(de) jaoks looma _Proxy Host_, millele pääseb ligi HTTP kaudu ja mis osutab sellele Nginxi installile. Pärast sertifikaadi väljastamist saate muuta _Proxy Host_'i, et seda sertifikaati ka HTTPS\nühenduste jaoks kasutada. Sertifikaadi uuendamiseks tuleb aga _Proxy Host_ ikkagi HTTP-juurdepääsu jaoks konfigureerida.\n\nSee protsess _ei_ toeta metamärke kasutavaid domeene.\n\n### DNS-sertifikaat\n\nDNS-i poolt valideeritud sertifikaadi saamiseks peate kasutama DNS-pakkuja pistikprogrammi. Seda DNS-teenuse pakkujat kasutatakse teie domeenis ajutiste kirjete loomiseks ja seejärel pärib Let's\nEncrypt nende kirjete kohta päringu, et veenduda, et olete omanik, ja kui see õnnestub, väljastavad nad teile sertifikaadi.\n\nSelle tüüpi sertifikaadi taotlemiseks ei ole vaja luua _Proxy Host_'i. Samuti ei pea teie _Proxy Host_ olema HTTP-juurdepääsu jaoks konfigureeritud.\n\nSee protsess _toetab_ metamärke kasutavaid domeene.\n\n### Kohandatud sertifikaat\n\nKasutage seda valikut oma SSL-sertifikaadi üleslaadimiseks, mille on esitanud teie enda sertifitseerimisasutus."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/DeadHosts.md",
    "content": "## Mis on 404 host?\n\n404 host on lihtsalt hosti seadistus, mis kuvab 404 lehte.\n\nSee võib olla kasulik, kui teie domeen on otsingumootorites loetletud ja soovite\nesitada kenama vealehe või konkreetselt otsingu indekseerijatele öelda, et\ndomeenilehed enam ei eksisteeri.\n\nSelle hosti teine eelis on selle külastatavuste logide jälgimine ja suunajate vaatamine."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/ProxyHosts.md",
    "content": "## Mis on puhverserver?\n\nPuhverserver on veebiteenuse sissetuleva andmevoo lõpp-punkt, mida soovite edastada.\n\nSee pakub valikulist SSL-i lõpetamist teie teenusele, millel ei pruugi olla sisseehitatud SSL-tuge.\n\nPuhverserverid on Nginxi puhverserveri halduri kõige levinum kasutusala."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/RedirectionHosts.md",
    "content": "## Mis on ümbersuunamishost?\n\nÜmbersuunamishost suunab sissetuleva domeeni päringud ümber ja suunab vaataja teisele domeenile.\n\nKõige levinum põhjus seda tüüpi hosti kasutamiseks on see, kui teie veebisaidi domeenid muutuvad, kuid otsingumootori või suunaja lingid osutavad endiselt vanale domeenile."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/Streams.md",
    "content": "## Mis on voog?\n\nNginxi suhteliselt uus funktsioon, voog, edastab TCP/UDP liiklust otse võrgus olevale teisele arvutile.\n\nKui sul on mänguserverid, FTP- või SSH-serverid, võib see kasuks tulla."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/et/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/AccessLists.md",
    "content": "## Qu'est-ce qu'une liste d'accès ?\n\nLes listes d'accès permettent de définir une liste noire ou une liste blanche d'adresses IP clientes spécifiques, ainsi que l'authentification des Hôtes Proxy via l'authentification HTTP de base.\n\nVous pouvez configurer plusieurs règles client, noms d'utilisateur et mots de passe pour une même liste d'accès, puis l'appliquer à un ou plusieurs Hôtes Proxy.\n\nCeci est particulièrement utile pour les services web redirigés qui ne disposent pas de mécanismes d'authentification intégrés ou lorsque vous souhaitez vous protéger contre les clients inconnus.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/Certificates.md",
    "content": "## Aide concernant les certificats\n\n### Certificat HTTP\n\nUn certificat HTTP validé signifie que les serveurs de Let's Encrypt testeront d'accéder à vos domaines via HTTP (et non HTTPS !). En cas de succès, ils émettront votre certificat.\n\nPour cette méthode, vous devrez créer un Hôte Proxy pour votre ou vos domaines. Cet Hôte Proxy devra être accessible via HTTP et pointer vers cette installation Nginx. Une fois le certificat émis, vous pourrez modifier l'Hôte Proxy pour qu'il utilise également ce certificat pour les connexions HTTPS. Cependant, l'Hôte Proxy devra toujours être configuré pour l'accès HTTP afin que le certificat puisse être renouvelé.\n\nCe processus ne prend pas en charge les domaines génériques.\n\n### Certificat DNS\n\nUn certificat DNS validé nécessite l'utilisation du plugin Fournisseur DNS. Fournisseur DNS créera des enregistrements temporaires sur votre domaine. Let's Encrypt interrogera ensuite ces enregistrements pour vérifier que vous en êtes bien le propriétaire. En cas de succès, votre certificat sera émis.\n\nIl n'est pas nécessaire de créer un Hôte Proxy avant de demander ce type de certificat.\n\nIl n'est pas non plus nécessaire de configurer votre Hôte Proxy pour l'accès HTTP.\n\nCe processus prend en charge les domaines génériques.\n\n## Certificat personnalisé\n\nUtilisez cette option pour importer votre propre certificat SSL, fourni par votre autorité de certification.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/DeadHosts.md",
    "content": "## Qu'est-ce qu'un serveur 404 ?\n\nUn Hôte 404 est simplement un hôte configuré pour afficher une page 404.\n\nCela peut s'avérer utile lorsque votre domaine est indexé par les moteurs de recherche et que vous souhaitez fournir une page d'erreur plus conviviale ou, plus précisément, indiquer aux moteurs de recherche que les pages du domaine n'existent plus.\n\nUn autre avantage de cet hôte est la possibilité de suivre les journaux et de consulter les sites référenceurs.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/ProxyHosts.md",
    "content": "## Qu'est-ce qu'un hôte proxy ?\n\nUn Hôte Proxy est le point de terminaison entrant d'un service web que vous souhaitez rediriger.\n\nIl assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement.\n\nLes Hôtes Proxy constituent l'utilisation la plus courante du Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/RedirectionHosts.md",
    "content": "## Qu'est-ce qu'un serveur de redirection ?\n\nUn Hôte de Redirection redirige les requêtes provenant du domaine entrant vers un autre domaine.\n\nOn utilise généralement ce type d'hôte lorsque votre site web change de domaine, mais que des liens provenant des moteurs de recherche ou des sites référenceurs pointent toujours vers l'ancien domaine.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/Streams.md",
    "content": "## Qu'est-ce qu'un Stream ?\n\nFonctionnalité relativement récente de Nginx, un Stream permet de rediriger le trafic TCP/UDP directement vers un autre ordinateur du réseau.\n\nSi vous gérez des serveurs de jeux, FTP ou SSH, cela peut s'avérer très utile.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/fr/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/AccessLists.md",
    "content": "## Cad is Liosta Rochtana ann?\n\nSoláthraíonn Liostaí Rochtana liosta dubh nó liosta bán de sheoltaí IP cliant ar leith mar aon le fíordheimhniú do na hÓstaigh Seachfhreastalaí trí Fhíordheimhniú Bunúsach HTTP.\n\nIs féidir leat rialacha cliant, ainmneacha úsáideora agus pasfhocail iolracha a chumrú le haghaidh Liosta Rochtana aonair agus ansin iad sin a chur i bhfeidhm ar _Óstach Seachfhreastalaí_ amháin nó níos mó.\n\nTá sé seo an-úsáideach i gcás seirbhísí gréasáin atreoraithe nach bhfuil meicníochtaí fíordheimhnithe ionsuite iontu nó nuair is mian leat cosaint a dhéanamh ar chliaint anaithnide.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/Certificates.md",
    "content": "## Cabhair le Deimhnithe\n\n### Teastas HTTP\n\nCiallaíonn deimhniú bailíochtaithe HTTP go ndéanfaidh freastalaithe Let's Encrypt iarracht teacht ar do fhearainn thar HTTP (ní HTTPS!) agus má éiríonn leo, eiseoidh siad do theastas.\n\nChun an modh seo a dhéanamh, beidh ort _Óstach Proxy_ a chruthú do do fhearainn(eanna) atá inrochtana le HTTP agus ag pointeáil chuig an suiteáil Nginx seo. Tar éis deimhniú a thabhairt, is féidir leat an _Óstach Proxy_ a mhodhnú chun an deimhniú seo a úsáid le haghaidh naisc HTTPS freisin. Mar sin féin, beidh ort an _Óstach Proxy_ a chumrú fós le haghaidh rochtain HTTP chun go ndéanfar an deimhniú a athnuachan.\n\n_Ní thacaíonn_ an próiseas seo le fearainn fiáine.\n\n### Teastas DNS\n\nÉilíonn deimhniú bailíochtaithe DNS ort breiseán Soláthraí DNS a úsáid. Úsáidfear an Soláthraí DNS seo chun taifid shealadacha a chruthú ar do fhearann agus ansin déanfaidh Let's Encrypt fiosrúchán ar na taifid sin lena chinntiú gurb tusa an t-úinéir agus má éiríonn leo, eiseoidh siad do theastas.\t\n\nNí gá duit _Óstach Proxy_ a chruthú sula n-iarrann tú an cineál seo teastais. Ní gá duit do _Óstach Proxy_ a chumrú le haghaidh rochtana HTTP ach an oiread.\n\n_Tacaíonn_ an próiseas seo le fearainn fiáine.\n\n### Teastas Saincheaptha\n\nÚsáid an rogha seo chun do Theastas SSL féin a uaslódáil, mar a sholáthraíonn d'Údarás Deimhnithe féin é."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/DeadHosts.md",
    "content": "## Cad is Óstach 404 ann?\n\nIs socrú óstach a thaispeánann leathanach 404 é Óstach 404.\n\nIs féidir leis seo a bheith úsáideach nuair a bhíonn do fhearann liostaithe in innill chuardaigh agus más mian leat leathanach earráide níos deise a sholáthar nó a chur in iúl do na hinnéacsóirí cuardaigh go sonrach nach bhfuil na leathanaigh fearainn ann a thuilleadh.\n\nBuntáiste eile a bhaineann leis an óstach seo a bheith agat ná go bhfeictear na logaí le haghaidh amas agus go bhfeictear na tagairtí."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md",
    "content": "## Cad is Óstach Seachfhreastalaí ann?\n\nIs é Óstach Seachfhreastalaí an críochphointe isteach do sheirbhís ghréasáin ar mhaith leat a atreorú.\n\nSoláthraíonn sé foirceannadh SSL roghnach do do sheirbhís nach bhfuil tacaíocht SSL ionsuite inti b'fhéidir.\n\nIs iad Óstaigh Seachfhreastalaí an úsáid is coitianta a bhaintear as Bainisteoir Seachfhreastalaí Nginx."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md",
    "content": "## Cad is Óstach Athsheolta ann?\n\nDéanfaidh Óstach Athsheolta iarratais a atreorú ón bhfearann ag teacht isteach agus an breathnóir a bhrú chuig fearann eile.\n\nIs é an chúis is coitianta le húsáid a bhaint as an gcineál seo óstála ná nuair a athraíonn do shuíomh Gréasáin fearainn ach go bhfuil naisc innill chuardaigh nó atreoraithe agat fós ag tagairt don seanfhearann.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/Streams.md",
    "content": "## Cad is Sruth ann?\n\nGné réasúnta nua do Nginx is ea Sruth a sheolfaidh trácht TCP/UDP go díreach chuig ríomhaire eile ar an líonra.\n\nMás freastalaithe cluichí, freastalaithe FTP nó SSH atá á rith agat, d’fhéadfadh sé seo a bheith úsáideach.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ga/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/AccessLists.md",
    "content": "## Mi az a hozzáférési lista?\n\nA hozzáférési listák feketelistát vagy fehérlistát biztosítanak meghatározott kliens IP-címekhez, valamint alap HTTP-hitelesítést (Basic HTTP Authentication) a proxy kiszolgálókhoz.\n\nEgyetlen hozzáférési listához több kliensszabályt, felhasználónevet és jelszót is beállíthatsz, majd ezt alkalmazhatod egy vagy több _Proxy Kiszolgáló_-ra.\n\nEz különösen hasznos olyan továbbított webszolgáltatásoknál, amelyekben nincs beépített hitelesítési mechanizmus, vagy amikor ismeretlen kliensektől szeretnél védeni.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/Certificates.md",
    "content": "## Tanúsítványok súgó\n\n### HTTP tanúsítvány\n\nA HTTP érvényes tanúsítvány azt jelenti, hogy a Let's Encrypt szerverek megpróbálják elérni a domaineket HTTP-n keresztül (nem HTTPS-en!), és ha sikerül, kiállítják a tanúsítványt.\n\nEhhez a módszerhez létre kell hoznod egy _Proxy Kiszolgáló_-t a domain(ek)hez, amely HTTP-n keresztül elérhető és erre az Nginx telepítésre mutat. Miután a tanúsítvány megérkezett, módosíthatod a _Proxy Kiszolgáló_-t, hogy ezt a tanúsítványt használja a HTTPS kapcsolatokhoz is. Azonban a _Proxy Kiszolgáló_-nak továbbra is konfigurálva kell lennie HTTP hozzáféréshez, hogy a tanúsítvány megújulhasson.\n\nEz a folyamat _nem_ támogatja a helyettesítő karakteres domaineket.\n\n### DNS tanúsítvány\n\nA DNS érvényes tanúsítvány megköveteli, hogy DNS szolgáltató plugint használj. Ez a DNS szolgáltató ideiglenes rekordokat hoz létre a domainen, majd a Let's Encrypt lekérdezi ezeket a rekordokat, hogy megbizonyosodjon a tulajdonjogról, és ha sikeres, kiállítják a tanúsítványt.\n\nNem szükséges előzetesen _Proxy Kiszolgáló_-t létrehozni az ilyen típusú tanúsítvány igényléséhez. Nem is kell a _Proxy Kiszolgáló_-t HTTP hozzáférésre konfigurálni.\n\nEz a folyamat _támogatja_ a helyettesítő karakteres domaineket.\n\n### Egyéni tanúsítvány\n\nEzt az opciót használd a saját SSL tanúsítvány feltöltéséhez, amelyet a saját tanúsítványkibocsátód biztosított.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/DeadHosts.md",
    "content": "## Mi az a 404-es Kiszolgáló?\n\nA 404-es Kiszolgáló egyszerűen egy olyan kiszolgáló beállítás, amely egy 404-es oldalt jelenít meg.\n\nEz akkor lehet hasznos, ha a domained szerepel a keresőmotorokban, és egy szebb hibaoldalt szeretnél nyújtani, vagy kifejezetten jelezni akarod a keresőrobotoknak, hogy a domain oldalai már nem léteznek.\n\nEnnek a kiszolgálónak egy további előnye, hogy nyomon követheted a rá érkező találatokat a naplókban, és megtekintheted a hivatkozó oldalakat.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/ProxyHosts.md",
    "content": "## Mi az a Proxy Kiszolgáló?\n\nA Proxy Kiszolgáló egy bejövő végpont egy olyan webszolgáltatáshoz, amelyet továbbítani szeretnél.\n\nOpcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás.\n\nA Proxy Kiszolgálók az Nginx Proxy Manager leggyakoribb felhasználási módjai.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/RedirectionHosts.md",
    "content": "## Mi az az Átirányító Kiszolgáló?\n\nAz Átirányító Kiszolgáló a bejövő domainre érkező kéréseket átirányítja, és a látogatót egy másik domainre küldi.\n\nEnnek a kiszolgálótípusnak a leggyakoribb használati oka az, amikor a weboldalad domaint vált, de a keresőkben vagy a hivatkozó oldalakon még mindig a régi domainre mutató linkek vannak.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/Streams.md",
    "content": "## Mi az a Stream?\n\nAz Nginx egy viszonylag új funkciója, a Stream arra szolgál, hogy a TCP/UDP forgalmat közvetlenül továbbítsa a hálózat egy másik számítógépére.\n\nHa játékszervereket, FTP vagy SSH szervereket futtatsz, ez hasznos lehet.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/hu/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/AccessLists.md",
    "content": "## Apa itu Daftar Akses?\n\nDaftar Akses menyediakan daftar hitam atau daftar putih alamat IP klien tertentu beserta autentikasi untuk Host Proxy melalui Autentikasi HTTP Basic.\n\nAnda dapat mengonfigurasi beberapa aturan klien, nama pengguna, dan kata sandi untuk satu Daftar Akses lalu menerapkannya ke satu atau lebih _Host Proxy_.\n\nIni paling berguna untuk layanan web yang diteruskan yang tidak memiliki mekanisme autentikasi bawaan atau ketika Anda ingin melindungi dari klien yang tidak dikenal.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/Certificates.md",
    "content": "## Bantuan Sertifikat\n\n### Sertifikat HTTP\n\nSertifikat yang divalidasi HTTP berarti server Let's Encrypt akan\nmencoba menjangkau domain Anda melalui HTTP (bukan HTTPS!) dan jika berhasil, mereka\nakan menerbitkan sertifikat Anda.\n\nUntuk metode ini, Anda harus membuat _Host Proxy_ untuk domain Anda yang\ndapat diakses dengan HTTP dan mengarah ke instalasi Nginx ini. Setelah sertifikat\ndiberikan, Anda dapat mengubah _Host Proxy_ agar juga menggunakan sertifikat ini untuk HTTPS\nkoneksi. Namun, _Host Proxy_ tetap perlu dikonfigurasi untuk akses HTTP\nagar sertifikat dapat diperpanjang.\n\nProses ini _tidak_ mendukung domain wildcard.\n\n### Sertifikat DNS\n\nSertifikat yang divalidasi DNS mengharuskan Anda menggunakan plugin Penyedia DNS. Penyedia DNS ini\nakan digunakan untuk membuat record sementara pada domain Anda dan kemudian Let's\nEncrypt akan menanyakan record tersebut untuk memastikan Anda pemiliknya dan jika berhasil, mereka\nakan menerbitkan sertifikat Anda.\n\nAnda tidak perlu membuat _Host Proxy_ sebelum meminta jenis sertifikat ini.\nAnda juga tidak perlu mengonfigurasi _Host Proxy_ untuk akses HTTP.\n\nProses ini _mendukung_ domain wildcard.\n\n### Sertifikat Kustom\n\nGunakan opsi ini untuk mengunggah Sertifikat SSL Anda sendiri, sebagaimana disediakan oleh\nCertificate Authority Anda.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/DeadHosts.md",
    "content": "## Apa itu Host 404?\n\nHost 404 adalah konfigurasi host yang menampilkan halaman 404.\n\nIni dapat berguna ketika domain Anda terindeks di mesin pencari dan Anda ingin\nmenyediakan halaman error yang lebih baik atau secara khusus memberi tahu pengindeks pencarian bahwa\nhalaman domain tersebut sudah tidak ada.\n\nManfaat lain memiliki host ini adalah melacak log untuk akses ke host tersebut dan\nmelihat perujuk.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/ProxyHosts.md",
    "content": "## Apa itu Host Proxy?\n\nHost Proxy adalah endpoint masuk untuk layanan web yang ingin Anda teruskan.\n\nHost ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan.\n\nHost Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md",
    "content": "## Apa itu Host Pengalihan?\n\nHost Pengalihan akan mengalihkan permintaan dari domain masuk dan mengarahkan pengunjung ke domain lain.\n\nAlasan paling umum menggunakan jenis host ini adalah ketika situs Anda berpindah domain tetapi masih ada tautan mesin pencari atau perujuk yang mengarah ke domain lama.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/Streams.md",
    "content": "## Apa itu Stream?\n\nFitur yang relatif baru untuk Nginx, Stream berfungsi untuk meneruskan trafik TCP/UDP\nlangsung ke komputer lain di jaringan.\n\nJika Anda menjalankan server game, FTP, atau SSH, ini bisa sangat membantu.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/id/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/index.ts",
    "content": "import * as bg from \"./bg/index\";\nimport * as de from \"./de/index\";\nimport * as pt from \"./pt/index\";\nimport * as en from \"./en/index\";\nimport * as es from \"./es/index\";\nimport * as et from \"./et/index\";\nimport * as fr from \"./fr/index\";\nimport * as ga from \"./ga/index\";\nimport * as id from \"./id/index\";\nimport * as it from \"./it/index\";\nimport * as ja from \"./ja/index\";\nimport * as ko from \"./ko/index\";\nimport * as nl from \"./nl/index\";\nimport * as pl from \"./pl/index\";\nimport * as ru from \"./ru/index\";\nimport * as sk from \"./sk/index\";\nimport * as cs from \"./cs/index\";\nimport * as vi from \"./vi/index\";\nimport * as zh from \"./zh/index\";\nimport * as tr from \"./tr/index\";\nimport * as hu from \"./hu/index\";\n\nconst items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu };\n\n\nconst fallbackLang = \"en\";\n\nexport const getHelpFile = (lang: string, section: string): string => {\n  if (typeof items[lang] !== \"undefined\" && typeof items[lang][section] !== \"undefined\") {\n    return items[lang][section].default;\n  }\n  // Fallback to English\n  if (typeof items[fallbackLang] !== \"undefined\" && typeof items[fallbackLang][section] !== \"undefined\") {\n    return items[fallbackLang][section].default;\n  }\n  throw new Error(`Cannot load help doc for ${lang}-${section}`);\n};\n\nexport default items;\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/AccessLists.md",
    "content": "## Che cos'è una Lista di Accesso?\n\nLa Lista di Accesso fornisce una blacklist o una whitelist di indirizzi IP specifici dei client insieme all'autenticazione per gli host proxy tramite autenticazione HTTP di base.\n\nÈ possibile configurare più regole client, nomi utente e password per un singolo lista di accesso e quindi applicarlo a uno o più host proxy.\n\nCiò è particolarmente utile per i servizi web inoltrati che non dispongono di meccanismi di autenticazione integrati o quando si desidera proteggersi da client sconosciuti.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/Certificates.md",
    "content": "## Guida sui Certificati\n\n### Certificato HTTP\n\nUn certificato convalidato HTTP significa che i server Let's Encrypttenteranno di raggiungere i tuoi domini tramite HTTP (non HTTPS!) e, in caso di esito positivo, emetteranno il tuo certificato.\n\nPer questo metodo, dovrai creare un _Proxy Host_ per i tuoi domini chesia accessibile con HTTP e che punti a questa installazione Nginx.\nDopo che il certificato è stato rilasciato, puoi modificare il _Proxy Host_ per utilizzare questo certificato anche per le connessioni HTTPS.\nTuttavia, il _Proxy Host_ dovrà comunque essere configurato per l'accesso HTTP affinché il certificato possa essere rinnovato.\n\nQuesto processo _non_ supporta i domini wildcard.\n\n### Certificato DNS\n\nUn certificato convalidato dal DNS richiede l'uso di un plugin DNS Provider. Questo DNS Provider verrà utilizzato per creare record temporanei sul tuo dominio,\nquindi Let's Encrypt interrogherà tali record per assicurarsi che tu sia il proprietario e, in caso di esito positivo,rilascerà il tuo certificato.\n\nNon è necessario creare un _Proxy Host_ prima di richiedere questo tipo di certificato. Non è nemmeno necessario configurare il tuo _proxy host_ per l'accesso HTTP.\n\nQuesto processo _supporta_ i domini wildcard.\n\n### Certificato personalizzato\n\nUtilizza questa opzione per caricare il tuo certificato SSL, fornito dalla tua autorità di certificazione.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/DeadHosts.md",
    "content": "## Che cos'è un Host 404?\n\nUn Host 404 è semplicemente una configurazione host che mostra una pagina 404.\n\nQuesto può essere utile quando il tuo dominio è elencato nei motori di ricerca e desideri fornire una pagina di errore più gradevole o specificare agli \nindicizzatori di ricerca che le pagine del dominio non esistono più.\n\nUn altro vantaggio di avere questo host è quello di tracciare i log degli accessi e\nvisualizzare i referrer.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/ProxyHosts.md",
    "content": "## Che cos'è un Proxy Host?\n\nUn host proxy è l'endpoint in entrata per un servizio web che si desidera inoltrare.\n\nFornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato.\n\nGli host proxy sono l'uso più comune per Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/RedirectionHosts.md",
    "content": "## Che cos'è un Host di reindirizzamento?\n\nUn Host di reindirizzamento reindirizza le richieste provenienti dal dominio in entrata e indirizza il\nvisitatore verso un altro dominio.\n\nIl motivo più comune per utilizzare questo tipo di host è quando il tuo sito web cambia\ndominio, ma hai ancora link di motori di ricerca o referrer che puntano al vecchio dominio.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/Streams.md",
    "content": "## Che cos'è uno Stream?\n\nUna funzionalità relativamente nuova per Nginx, uno Stream serve a inoltrare il traffico TCP/UDP\ndirettamente a un altro computer sulla rete.\n\nSe gestisci server di gioco, FTP o SSH, questa funzionalità può rivelarsi molto utile.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/it/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/AccessLists.md",
    "content": "## アクセスリストとは\n\nアクセスリストは特定のクライアントIPへのブラックリストとホワイトリストを提供し、ベーシック認証によるプロキシホストへの認証を可能にします。\n\n複数のクライアントルールやユーザー名とパスワードを一つのアクセスリストに設定し、一つ以上の _プロキシホスト_ に適応することができます。\n\nこれは認証システムを持たないサービスや不明なクライアントからの保護が必要な場合に有効です。\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/Certificates.md",
    "content": "## 証明書\n\n### HTTP 証明書\n\nHTTPによって検証された証明書はLet's EncryptサーバーがHTTPでドメインにアクセスを試みサーバーを管理していることを確認できた場合に発行される証明書です。\n\nこの方法では、HTTPアクセス可能でこのNginxを指しているドメインに対して _プロキシホスト_ を作成する必要があります。証明書が発行された後は、 _プロキシホスト_ を編集してこの証明書をHTTPS接続に使用するように設定できます。ただし、証明書の更新には、_プロキシホスト_ がHTTP接続用に設定された状態を維持する必要があります。\n\nこの方法はワイルドカードのドメインをサポート _していません_ 。\n\n### DNS 証明書\n\nDNSによって検証された証明書にはDNSプロバイダープラグインが必要です。このプロバイダーはドメイン上に一時レコードを作成するために使用されます。その後Let's Encryptサーバーがそのレコードを参照し、あなたが所有していることを確認できると証明書が発行されます。\n\nこのタイプの証明書を作成する際に、 _プロキシホスト_ を作成する必要はありません。また、_プロキシホスト_ をHTTPアクセス用に設定する必要もありません。\n\nこの方法はワイルドカードのドメインをサポート _します_ 。\n\n### カスタム証明書\n\nこのオプションでは、あなたの証明書認証局によって提供された自身の証明書をアップロードして使用できます。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/DeadHosts.md",
    "content": "## 404ホストとはなんですか?\n\n404ホストとは、単に404ページを表示するよう設定されたホストです。\n\nこれは、検索エンジンに登録されたドメインに分かりやすいエラーページを提供したい場合や、検索エンジンのインデクサーにドメインページがもう存在しないことを伝えたい場合に便利です。\n\nこのホストを持つもう一つの利点は、アクセスログを追跡し、参照元を確認できることです。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/ProxyHosts.md",
    "content": "## プロキシホストとは何ですか?\n\nプロキシホストは転送したいwebサービスの受信エンドポイントです。\n\nサービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。\n\nプロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/RedirectionHosts.md",
    "content": "## リダイレクトホストとは何ですか?\n\nリダイレクトホストは受信したリクエストを別のドメインにリダイレクトして訪問者に表示します。\n\nこのタイプのもっとも一般的な使用理由は、webサイトのドメインが変更されたが検索エンジンやリンクが古いドメインを指し続けている場合です。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/Streams.md",
    "content": "## ストリームとは何ですか?\n\nNginxの比較的新しい機能であるストリームは、TCP/UDPトラフィックをネットワーク上の別のコンピュータに直接転送します。\n\nゲームサーバー、FTPサーバー、SSHサーバーを運用している場合に便利です。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ja/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/AccessLists.md",
    "content": "## 접근 정책이란?\n\n접근 정책은 특정 클라이언트 IP 주소를 허용하거나 거부할 수 있으며,\n프록시 호스트에 기본 HTTP 인증(Basic Auth) 을 적용할 수 있는 기능입니다.\n\n하나의 접근 목록에 여러 클라이언트 규칙과 사용자 이름, 비밀번호를 추가한 뒤\n이를 하나 이상의 프록시 호스트에 적용할 수 있습니다.\n\n이 기능은 인증 기능이 없는 웹 서비스에 인증을 추가하거나,\n알 수 없는 클라이언트로부터 서비스를 보호할 때 유용합니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/Certificates.md",
    "content": "## 인증서 도움말\n\n### HTTP 인증서\n\nHTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.\n\n이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.\n\n다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**\n\n이 방식은 **와일드카드 도메인을 지원하지 않습니다.**\n\n---\n\n### DNS 인증서\n\nDNS 검증 방식의 인증서는 DNS 공급자 플러그인을 사용해야 합니다. 이 플러그인은 도메인에 임시 DNS 레코드를 생성하며, Let's Encrypt는 해당 레코드를 조회해 도메인 소유 여부를 확인합니다. 검증이 성공하면 인증서가 발급됩니다.\n\n이 방식은 인증서를 요청하기 전에 **프록시 호스트를 생성할 필요가 없으며**, 프록시 호스트에 HTTP 접근을 설정할 필요도 없습니다.\n\n이 방식은 **와일드카드 도메인을 지원합니다.**\n\n---\n\n### 사용자 지정 인증서\n\n이 옵션을 사용하면 직접 보유한 인증 기관(CA)에서 발급한 SSL 인증서를 직접 업로드하여 사용할 수 있습니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/DeadHosts.md",
    "content": "## 404 호스트란?\n\n404 호스트는 404 오류 페이지를 표시하도록 구성된 호스트입니다.\n\n이 기능은 도메인이 검색 엔진에 이미 색인되어 있을 때,\n더 깔끔한 오류 페이지를 제공하거나 해당 페이지가 더 이상 존재하지 않음을\n검색 엔진에게 명확하게 알려야 할 때 유용합니다.\n\n또한 404 호스트를 사용하면 접근 로그를 확인하고, 어떤 경로(Referrer)를 통해 들어왔는지 추적할 수 있다는 장점도 있습니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md",
    "content": "## 프록시 호스트란?\n\n프록시 호스트는 외부에서 들어오는 웹 요청을 받아 지정한 전달 대상으로 전달하는 역할을 합니다.\n\n원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.\n\n프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md",
    "content": "## 리다이렉션 호스트란?\n\n리다이렉션 호스트는 외부에서 들어오는 도메인 요청을 다른 도메인으로 자동 이동(리다이렉트)시키는 역할을 합니다.\n\n이 유형의 호스트는 주로 웹사이트의 도메인이 변경되었지만,\n검색 엔진이나 다른 사이트에 이전 도메인 링크가 남아 있을 때 사용하면 가장 효과적입니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/Streams.md",
    "content": "## 호스트 스트림이란?\n\n호스트 스트림은 비교적 최근에 Nginx에 추가된 기능으로,\nTCP/UDP 트래픽을 네트워크 내의 다른 컴퓨터로 직접 전달하는 데 사용됩니다.\n\n게임 서버나 FTP, SSH 서버 등을 운영할 때 유용하게 사용할 수 있습니다.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ko/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/AccessLists.md",
    "content": "## Wat is een Toegangslijst?\n\nToeganslijsten bieden een zwarte- of witte lijst van specifieke client IP-adressen samen met authenticatie voor de Proxy Hosts via Basic HTTP Authenticatie.\n\nJe kan meerdere client regels, gebruikersnamen en wachtwoorden voor een enkele Toegangslijst configureren en toepassen op één of meerdere _Proxy Hosts_.\n\nDit is het meest nuttig voor doorgestuurde webdiensten die geen authenticatiemechanismen hebben of wanneer je wilt beveiligen tegen onbekende bezoekers.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/Certificates.md",
    "content": "## Certificaten Hulp\n\n### HTTP Certificaat\n\nEen HTTP gevalideerd certificaat betekent dat Let's Encrypt servers\nzullen proberen om over HTTP te bereiken (niet HTTPS!) en als dat gelukt is, zal\njouw certificaat worden uitgegeven.\n\nVoor deze zal je een _Proxy Host_ moeten hebben die is toegankelijk via HTTP en\ndie naar deze Nginx installatie wijst. Nadat een certificaat is uitgegeven kan je\nde _Proxy Host_ wijzigen om ook HTTPS toegang te geven. Maar de _Proxy Host_ zal\nnog moeten worden geconfigureerd voor HTTP toegang om het certificaat te verlengen.\n\nDit proces ondersteunt geen domeinen met wildcards.\n\n### DNS Certificaat\n\nEen DNS gevalideerd certificaat zal gebruik maken van een DNS Provider plugin. De\nDNS Provider zal tijdelijke records op jouw domein maken en Let's Encrypt zal deze\nrecords opvragen om te controleren of je de eigenaar bent. Als dat is gecontroleerd\nis zal Let's Encrypt het certificaat uitgeven.\n\nJe hebt geen _Proxy Host_ nodig om dit soort certificaat aan te vragen. Je hebt dus\ngeen HTTP _Proxy Host_ nodig.\n\nDit proces ondersteunt _wel_ domeinen met wildcards.\n\n### Aangepast Certificaat\n\nGebruik deze optie om jouw eigen SSL Certificaat te uploaden, zoals\ngeleverd door jouw eigen Certificate Authority.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/DeadHosts.md",
    "content": "## Wat is een 404 Host?\n\nSimpel gezegd is een 404 Host een host setup die een 404 pagina weergeeft.\n\nDit kan nuttig zijn wanneer jouw domein is opgegeven in zoekmachines en je wil\neen betere foutpagina leveren of specifiek om te zeggen tegen de zoekmachines dat\nhet domein niet langer bestaat.\n\nEen ander voordeel van het hebben van een 404 Host is om de logs voor bezoeken\nte volgen en de referenties te bekijken.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/ProxyHosts.md",
    "content": "## Wat is een Proxy Host?\n\nEen Proxy Host is de inkomende endpoint voor een webdienst dat je wilt doorsturen.\n\nHet biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft.\n\nProxy Hosts worden het meest gebruikt in Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/RedirectionHosts.md",
    "content": "## Wat is een Redirection Host?\n\nEen Redirection Host zal verzoeken van de inkomende domeinnaam doorsturen, en de bezoeker\nomleiden naar een andere domeinnaam.\n\nHet gebruik van een Redirection Host is vooral handig wanneer je jouw website verandert\nmaar je nog zoekmachines of referenties naar de oude domeinnaam hebben.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/Streams.md",
    "content": "## Wat is een Stream?\n\nStreams zijn een nieuwe toevoeging aan Nginx, die toelaat om TCP/UDP\nverkeer naar een ander computer op het netwerk te sturen.\n\nAls je game servers, FTP of SSH servers draait kan dit handig zijn.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/nl/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/AccessLists.md",
    "content": "## Hva er en tilgangsliste?\n\nTilgangslister gir en svarteliste eller hviteliste over spesifikke klient‑IP‑adresser, sammen med autentisering for `Proxy‑hosts` via Basic HTTP‑autentisering.\n\nDu kan konfigurere flere klientregler, brukernavn og passord for én tilgangsliste og deretter bruke denne på én eller flere `Proxy‑hosts`.\n\nDette er spesielt nyttig for videresendte webtjenester som ikke har innebygd autentisering, eller når du ønsker å beskytte mot ukjente klienter.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/Certificates.md",
    "content": "## Hjelp om sertifikater\n\n### HTTP‑sertifikat\n\nEt HTTP‑validert sertifikat betyr at Let's Encrypt‑serverne vil forsøke å nå\ndomenene dine over HTTP (ikke HTTPS!) og hvis det lykkes, vil de utstede sertifikatet.\n\nFor denne metoden må du ha en `Proxy‑host` opprettet for domenet/domenene dine som\ner tilgjengelig over HTTP og peker til denne Nginx‑installasjonen. Etter at et sertifikat\ner utstedt, kan du endre `Proxy‑host` til også å bruke dette sertifikatet for HTTPS‑tilkoblinger.\nProxy‑hosten må imidlertid fortsatt være konfigurert for HTTP‑tilgang for at sertifikatet skal kunne fornyes.\n\nDenne prosessen _støtter ikke_ wildcard‑domener.\n\n### DNS‑sertifikat\n\nEt DNS‑validert sertifikat krever at du bruker en DNS‑leverandør‑plugin. Denne leverandøren\nvil opprette midlertidige DNS‑poster på domenet ditt, og Let's Encrypt vil deretter spørre\ndisse postene for å bekrefte at du eier domenet. Hvis valideringen lykkes, utstedes sertifikatet.\n\nDu trenger ikke å ha en `Proxy‑host` opprettet før du ber om denne typen sertifikat. Du trenger heller\nikke at `Proxy‑host` er konfigurert for HTTP‑tilgang.\n\nDenne prosessen _støtter_ wildcard‑domener.\n\n### Egendefinert sertifikat\n\nBruk dette alternativet for å laste opp ditt eget SSL‑sertifikat, levert av din\negen sertifikatmyndighet (CA).\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/DeadHosts.md",
    "content": "## Hva er en 404‑host?\n\nEn 404‑host er enkelt og greit en host‑oppsett som viser en 404‑side.\n\nDette kan være nyttig når domenet ditt er oppført i søkemotorer og du ønsker å\nvise en penere feilmelding, eller for å fortelle søkeindekser at sidene på domenet\nikke lenger eksisterer.\n\nEn annen fordel med å ha denne hosten er å kunne spore treff i loggene og\nse hvilke henvisere som kommer til den.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/ProxyHosts.md",
    "content": "## Hva er en Proxy‑host?\n\nEn Proxy‑host er inngangspunktet (innkommende endepunkt) for en webtjeneste du ønsker å videresende.\n\nDen tilbyr valgfri SSL‑terminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL.\n\nProxy‑hosts er den vanligste bruken av Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/RedirectionHosts.md",
    "content": "## Hva er en omdirigerings‑host?\n\nEn omdirigerings‑host omdirigerer forespørsler fra det innkommende domenet og videresender\nbrukeren til et annet domene.\n\nDen vanligste årsaken til å bruke denne typen host er når nettstedet ditt har byttet\ndomene, men søkemotorer eller henvisningslenker fortsatt peker til det gamle domenet.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/Streams.md",
    "content": "## Hva er en Stream?\n\nEn relativt ny funksjon i Nginx. En Stream brukes til å videresende TCP/UDP‑trafikk\ndirekte til en annen maskin i nettverket.\n\nDette er nyttig hvis du kjører spillservere, FTP‑ eller SSH‑servere.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/no/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/AccessLists.md",
    "content": "## Czym jest lista dostępu?\n\nListy dostępu zapewniają czarną lub białą listę określonych adresów IP klientów wraz z uwierzytelnianiem dla hostów proxy za pomocą podstawowego uwierzytelniania HTTP.\n\nMożesz skonfigurować wiele reguł klienta, nazw użytkowników i haseł dla pojedynczej listy dostępu, a następnie zastosować ją do jednego lub więcej hostów proxy.\n\nJest to najbardziej przydatne w przypadku przekierowywanych usług internetowych, które nie mają wbudowanych mechanizmów uwierzytelniania lub gdy chcesz zabezpieczyć się przed nieznanymi klientami."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/Certificates.md",
    "content": "## Pomoc dotycząca certyfikatów\n\n### Certyfikat HTTP\n\nCertyfikat weryfikowany przez HTTP oznacza, że serwery Let's Encrypt będą próbowały połączyć się z twoimi domenami przez HTTP (nie HTTPS!) i jeśli się to powiedzie, wydadzą twój certyfikat.\n\nW przypadku tej metody musisz mieć utworzony Host proxy dla swoich domen, który jest dostępny przez HTTP i wskazuje na tę instalację Nginx. \nPo otrzymaniu certyfikatu możesz zmodyfikować Host proxy, aby używał również tego certyfikatu do połączeń HTTPS. Jednak Host proxy nadal będzie musiał być skonfigurowany do dostępu przez HTTP, aby certyfikat mógł być odnawiany.\n\nTen proces nie obsługuje domen wieloznacznych (wildcard).\n\n### Certyfikat DNS\n\nCertyfikat weryfikowany przez DNS wymaga użycia wtyczki dostawcy DNS. Ten dostawca DNS zostanie użyty do utworzenia tymczasowych rekordów w twojej domenie, a następnie Let's Encrypt sprawdzi te rekordy, aby upewnić się, że jesteś właścicielem i jeśli się powiedzie, wydadzą twój certyfikat.\n\nNie musisz mieć utworzonego Hosta proxy przed wystąpieniem o ten typ certyfikatu. Nie musisz również mieć skonfigurowanego Hosta proxy do dostępu przez HTTP.\n\nTen proces obsługuje domeny wieloznaczne (wildcard).\n\n### Własny certyfikat\n\nUżyj tej opcji, aby przesłać własny certyfikat SSL, dostarczony przez twój własny urząd certyfikacji.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/DeadHosts.md",
    "content": "## Czym jest host 404?\n\nHost 404 to po prostu konfiguracja hosta, która wyświetla stronę 404.\n\nMoże to być przydatne, gdy twoja domena jest indeksowana w wyszukiwarkach i chcesz zapewnić ładniejszą stronę błędu lub konkretnie poinformować roboty indeksujące, że strony domeny już nie istnieją.\n\nKolejną zaletą posiadania tego hosta jest możliwość śledzenia logów dla odwiedzin oraz przeglądania źródeł ruchu (referrerów)."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/ProxyHosts.md",
    "content": "## Czym jest host proxy?\n\nHost proxy to punkt wejściowy dla usługi internetowej, którą chcesz przekierować.\n\nZapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL.\n\nHosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/RedirectionHosts.md",
    "content": "## Czym jest host przekierowania?\n\nHost przekierowania przekierowuje żądania z domeny przychodzącej i przenosi odwiedzającego na inną domenę.\n\nNajczęstszym powodem używania tego typu hosta jest sytuacja, gdy twoja strona internetowa zmienia domeny, ale nadal masz linki z wyszukiwarek lub odnośniki wskazujące na starą domenę."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/Streams.md",
    "content": "## Czym jest strumień?\n\nStosunkowo nowa funkcja dla Nginx, strumień służy do przekazywania ruchu TCP/UDP bezpośrednio na inny komputer/serwer w sieci.\n\nJeśli prowadzisz serwery gier, FTP lub SSH, może się to okazać przydatne"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pl/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/AccessLists.md",
    "content": "## O que é uma Access List?\n\nAs *Access Lists* fornecem uma lista de permissões (whitelist) ou bloqueios (blacklist)\nde endereços IP específicos de clientes, juntamente com autenticação para os *Proxy Hosts*\nvia Autenticação HTTP Básica (*Basic Auth*).\n\nPodes configurar múltiplas regras de cliente, nomes de utilizador e palavras-passe\npara uma única *Access List*, e depois aplicá-la a um ou mais *Proxy Hosts*.\n\nIsto é especialmente útil para serviços web encaminhados que não têm mecanismos\nde autenticação integrados ou quando pretendes proteger o acesso contra clientes desconhecidos.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/Certificates.md",
    "content": "## Ajuda de Certificados\n\n### Certificado HTTP\n\nUm certificado validado por HTTP significa que os servidores do Let's Encrypt irão\ntentar aceder aos teus domínios via HTTP (não HTTPS!) e, se a ligação for bem-sucedida,\nemitirão o certificado.\n\nPara este método, é necessário ter um *Proxy Host* criado para o(s) teu(s) domínio(s),\nacessível via HTTP e a apontar para esta instalação do Nginx. Depois de o certificado ser\nemitido, podes modificar o *Proxy Host* para também utilizar esse certificado em ligações HTTPS.\nNo entanto, o *Proxy Host* deve continuar configurado para acesso HTTP para que a renovação\nfuncione corretamente.\n\nEste processo **não** suporta domínios wildcard.\n\n### Certificado DNS\n\nUm certificado validado por DNS requer que uses um plugin de fornecedor DNS (*DNS Provider*).\nEste fornecedor será usado para criar registos temporários no teu domínio, que serão consultados\npelo Let's Encrypt para confirmar que és o proprietário. Se tudo correr bem, o certificado será emitido.\n\nNão é necessário ter um *Proxy Host* criado antes de pedir este tipo de certificado.\nTambém não é necessário que o *Proxy Host* tenha acesso HTTP configurado.\n\nEste processo **suporta** domínios wildcard.\n\n### Certificado Personalizado\n\nUsa esta opção para carregar o teu próprio Certificado SSL, fornecido pela\ntua Autoridade Certificadora.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/DeadHosts.md",
    "content": "## O que é um 404 Host?\n\nUm *404 Host* é simplesmente um host configurado para apresentar uma página 404.\n\nIsto pode ser útil quando o teu domínio aparece em motores de busca e queres fornecer\numa página de erro mais agradável ou indicar especificamente aos indexadores de pesquisa\nque as páginas desse domínio já não existem.\n\nOutra vantagem é permitir consultar os registos de acessos a este host e ver os referenciadores.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/ProxyHosts.md",
    "content": "## O que é um Proxy Host?\n\nUm *Proxy Host* é o ponto de entrada para um serviço web que pretendes encaminhar.\n\nPermite, opcionalmente, fazer terminação SSL para um serviço que possa não ter suporte SSL nativo.\n\nOs *Proxy Hosts* são a utilização mais comum do Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/RedirectionHosts.md",
    "content": "## O que é um Redirection Host?\n\nUm *Redirection Host* redireciona pedidos recebidos no domínio de entrada e envia\no utilizador para outro domínio.\n\nA razão mais comum para usar este tipo de host é quando o teu site muda de domínio\nmas ainda tens motores de busca ou links de referência a apontar para o domínio antigo.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/Streams.md",
    "content": "## O que é um Stream?\n\nUma funcionalidade relativamente recente no Nginx, um *Stream* serve para encaminhar\ntráfego TCP/UDP diretamente para outro computador na rede.\n\nSe estiveres a executar servidores de jogos, FTP ou SSH, isto pode ser bastante útil.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/pt/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/AccessLists.md",
    "content": "## Что такое список доступа?\n\nСписки доступа позволяют задавать белый/чёрный список IP‑адресов клиентов и настраивать аутентификацию для прокси‑хостов через базовую HTTP‑аутентификацию.\n\nДля одного списка доступа можно настроить несколько правил клиентов, логины и пароли, а затем применить его к одному или нескольким _прокси‑хостам_.\n\nЭто особенно полезно для проксируемых веб‑сервисов без встроенной аутентификации или когда нужно защититься от неизвестных клиентов.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/Certificates.md",
    "content": "## Справка по сертификатам\n\n### HTTP-сертификат\n\nСертификат, подтверждённый по HTTP, означает, что серверы Let's Encrypt попытаются обратиться к вашим доменам по HTTP (не HTTPS!) и при успехе выпустят сертификат.\n\nДля этого метода должен существовать _прокси‑хост_ для ваших доменов, доступный по HTTP и указывающий на эту установку Nginx. После выдачи сертификата вы можете настроить _прокси‑хост_ на использование этого сертификата для HTTPS‑подключений. Однако доступ по HTTP должен сохраняться, чтобы сертификат мог обновляться.\n\nЭтот способ _не_ поддерживает wildcard‑домены.\n\n### DNS-сертификат\n\nСертификат, подтверждённый по DNS, требует использования плагина DNS‑провайдера. Такой провайдер создаст временные записи в вашем домене, затем Let's Encrypt проверит эти записи, чтобы убедиться, что вы владелец домена, и при успехе выпустит сертификат.\n\nДля запроса такого сертификата предварительно создавать _прокси‑хост_ не требуется. Также не нужен доступ по HTTP для вашего _прокси‑хоста_.\n\nЭтот способ _поддерживает_ wildcard‑домены.\n\n### Свой сертификат\n\nИспользуйте этот вариант, чтобы загрузить собственный SSL‑сертификат, выданный вашим удостоверяющим центром (CA).\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/DeadHosts.md",
    "content": "## Что такое 404‑хост?\n\n404‑хост — это конфигурация, которая показывает страницу 404.\n\nЭто полезно, когда ваш домен присутствует в поисковых системах и вы хотите показать более дружелюбную страницу ошибки или явно сообщить индексаторам, что страницы домена больше не существуют.\n\nЕщё одно преимущество — можно отдельно отслеживать обращения в журналах и смотреть источники переходов.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/ProxyHosts.md",
    "content": "## Что такое прокси‑хост?\n\nПрокси‑хост — это входная точка веб‑сервиса, который вы проксируете.\n\nОн может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL.\n\nПрокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/RedirectionHosts.md",
    "content": "## Что такое редирект‑хост?\n\nРедирект‑хост перенаправляет запросы, поступающие на входящий домен, на другой домен.\n\nЧаще всего это используют, когда сайт сменил домен, а в поиске или на сторонних ресурсах всё ещё остаются ссылки на старый домен.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/Streams.md",
    "content": "## Что такое поток?\n\nОтносительно новая возможность Nginx: поток позволяет напрямую проксировать TCP/UDP‑трафик на другой компьютер в сети.\n\nПолезно для игровых серверов, FTP или SSH‑серверов.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/ru/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/AccessLists.md",
    "content": "## Čo je zoznam prístupov?\n\nZoznamy prístupov poskytujú čiernu alebo bielu listinu konkrétnych IP adries klientov spolu s overovaním pre proxy hostiteľov prostredníctvom základného overovania HTTP.\n\nMôžete nakonfigurovať viacero pravidiel pre klientov, používateľských mien a hesiel pre jeden zoznam prístupov a potom ho použiť na jeden alebo viacero proxy hostiteľov.\n\nToto je najužitočnejšie pre presmerované webové služby, ktoré nemajú zabudované overovacie mechanizmy, alebo ak sa chcete chrániť pred neznámymi klientmi.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/Certificates.md",
    "content": "## Pomoc s certifikátmi\n\n### Certifikát HTTP\n\nCertifikát overený protokolom HTTP znamená, že servery Let's Encrypt sa\npokúsia pripojiť k vašim doménam cez protokol HTTP (nie HTTPS!) a v prípade úspechu\nvydajú váš certifikát.\n\nPre túto metódu budete musieť mať pre svoje domény vytvorený _Proxy Host_, ktorý\nje prístupný cez HTTP a smeruje na túto inštaláciu Nginx. Po vydaní certifikátu\nmôžete zmeniť _Proxy Host_ tak, aby tento certifikát používal aj pre HTTPS\npripojenia. _Proxy Host_ však bude stále potrebné nakonfigurovať pre prístup cez HTTP,\naby sa certifikát mohol obnoviť.\n\nTento proces _nepodporuje_ domény s divokými kartami.\n\n### Certifikát DNS\n\nCertifikát overený DNS vyžaduje použitie pluginu DNS Provider. Tento DNS\nProvider sa použije na vytvorenie dočasných záznamov vo vašej doméne a potom Let's\nEncrypt overí tieto záznamy, aby sa uistil, že ste vlastníkom, a ak bude úspešný,\nvydá váš certifikát.\n\nPred požiadaním o tento typ certifikátu nie je potrebné vytvoriť _Proxy Host_.\nTiež nie je potrebné mať _Proxy Host_ nakonfigurovaný pre prístup HTTP.\n\nTento proces _podporuje_ domény s divokými kartami.\n\n### Vlastný certifikát\n\nTúto možnosť použite na nahratie vlastného SSL certifikátu, ktorý vám poskytla vaša\ncertifikačná autorita.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/DeadHosts.md",
    "content": "## Čo je to 404 Hostiteľ?\n\n404 Hostiteľ je jednoducho nastavenie hostiteľa, ktoré zobrazuje stránku 404.\n\nTo môže byť užitočné, ak je vaša doména uvedená vo vyhľadávačoch a chcete\nposkytnúť krajšiu stránku s chybou alebo konkrétne oznámiť vyhľadávačom, že\nstránky domény už neexistujú.\n\nĎalšou výhodou tohto hostiteľa je sledovanie protokolov o návštevách a\nzobrazenie odkazov.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/ProxyHosts.md",
    "content": "## Čo je proxy hostiteľ?\n\nProxy hostiteľ je prichádzajúci koncový bod pre webovú službu, ktorú chcete presmerovať.\n\nPoskytuje voliteľné ukončenie SSL pre vašu službu, ktorá nemusí mať zabudovanú podporu SSL.\n\nProxy hostitelia sú najbežnejším použitím pre Nginx Proxy Manager.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/RedirectionHosts.md",
    "content": "## Čo je presmerovací hostiteľ?\n\nPresmerovací hostiteľ presmeruje požiadavky z prichádzajúcej domény a presmeruje\nnávštevníka na inú doménu.\n\nNajčastejším dôvodom na použitie tohto typu hostiteľa je situácia, keď vaša webová stránka zmení\ndoménu, ale stále máte odkazy vo vyhľadávačoch alebo referenčné odkazy smerujúce na starú doménu.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/Streams.md",
    "content": "## Čo je stream?\n\nStream je relatívne nová funkcia pre Nginx, ktorá slúži na presmerovanie TCP/UDP\ndátového toku priamo do iného počítača v sieti.\n\nAk prevádzkujete herné servery, FTP alebo SSH servery, táto funkcia sa vám môže hodiť.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/sk/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/AccessLists.md",
    "content": "## Erişim Listesi Nedir?\n\nErişim Listeleri, Temel HTTP Kimlik Doğrulama aracılığıyla Proxy Host'lar için belirli istemci IP adreslerinin kara listesi veya beyaz listesini ve kimlik doğrulamasını sağlar.\n\nTek bir Erişim Listesi için birden fazla istemci kuralı, kullanıcı adı ve şifre yapılandırabilir ve bunu bir veya daha fazla _Proxy Host_'a uygulayabilirsiniz.\n\nBu, yerleşik kimlik doğrulama mekanizmaları olmayan veya bilinmeyen istemcilerden korunmak istediğinizde iletilen web hizmetleri için en kullanışlıdır.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/Certificates.md",
    "content": "## Sertifika Yardımı\n\n### HTTP Sertifikası\n\nBir HTTP doğrulanmış sertifika, Let's Encrypt sunucularının\nalan adlarınıza HTTP (HTTPS değil!) üzerinden ulaşmaya çalışacağı ve başarılı olursa,\nsertifikanızı verecekleri anlamına gelir.\n\nBu yöntem için, alan adlarınız için HTTP ile erişilebilir ve bu Nginx kurulumuna işaret eden bir _Proxy Host_ oluşturulmuş olmalıdır. Bir sertifika\nverildikten sonra, _Proxy Host_'u HTTPS\nbağlantıları için de bu sertifikayı kullanacak şekilde değiştirebilirsiniz. Ancak, sertifikanın yenilenmesi için _Proxy Host_'un hala HTTP erişimi için yapılandırılmış olması gerekecektir.\n\nBu işlem joker karakter alan adlarını _desteklemez_.\n\n### DNS Sertifikası\n\nBir DNS doğrulanmış sertifika, bir DNS Sağlayıcı eklentisi kullanmanızı gerektirir. Bu DNS\nSağlayıcı, alan adınızda geçici kayıtlar oluşturmak için kullanılacak ve ardından Let's\nEncrypt bu kayıtları sorgulayarak sahibi olduğunuzdan emin olacak ve başarılı olursa,\nsertifikanızı verecektir.\n\nBu tür bir sertifika talep etmeden önce bir _Proxy Host_ oluşturulmasına gerek yoktur. Ayrıca _Proxy Host_'unuzun HTTP erişimi için yapılandırılmasına da gerek yoktur.\n\nBu işlem joker karakter alan adlarını _destekler_.\n\n### Özel Sertifika\n\nKendi Sertifika Otoriteniz tarafından sağlanan kendi SSL Sertifikanızı yüklemek için bu seçeneği kullanın.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/DeadHosts.md",
    "content": "## 404 Host Nedir?\n\n404 Host, basitçe bir 404 sayfası gösteren bir host kurulumudur.\n\nBu, alan adınız arama motorlarında listelendiğinde ve daha güzel bir hata sayfası sağlamak veya özellikle arama dizinleyicilerine\nalan adı sayfalarının artık mevcut olmadığını söylemek istediğinizde yararlı olabilir.\n\nBu host'un bir başka faydası da, ona yapılan isteklerin loglarını takip etmek ve\nreferansları görüntülemektir.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/ProxyHosts.md",
    "content": "## Proxy Host Nedir?\n\nProxy Host, iletilmek istediğiniz bir web hizmeti için gelen uç noktadır.\n\nSSL desteği yerleşik olmayan hizmetiniz için isteğe bağlı SSL sonlandırma sağlar.\n\nProxy Host'lar, Nginx Proxy Manager'ın en yaygın kullanımıdır.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/RedirectionHosts.md",
    "content": "## Yönlendirme Host'u Nedir?\n\nYönlendirme Host'u, gelen alan adından gelen istekleri yönlendirir ve\ngörüntüleyiciyi başka bir alan adına yönlendirir.\n\nBu tür bir host kullanmanın en yaygın nedeni, web sitenizin alan adı değiştiğinde\nancak hala eski alan adına işaret eden arama motoru veya referans bağlantılarınız olduğunda ortaya çıkar.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/Streams.md",
    "content": "## Akış Nedir?\n\nNginx için nispeten yeni bir özellik olan Akış, TCP/UDP\ntrafiğini doğrudan ağdaki başka bir bilgisayara iletmek için hizmet edecektir.\n\nOyun sunucuları, FTP veya SSH sunucuları çalıştırıyorsanız bu işinize yarayabilir.\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/tr/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/AccessLists.md",
    "content": "## Khái niệm Access List là gì?\n\nAccess List (Danh sách truy cập) cung cấp cơ chế chặn (blacklist) hoặc cho phép (whitelist) các địa chỉ IP của client, đồng thời hỗ trợ xác thực Basic HTTP Authentication cho các Proxy Host.\n\nBạn có thể cấu hình nhiều quy tắc client, nhiều tên người dùng và mật khẩu trong một Access List duy nhất, sau đó áp dụng Access List đó cho một hoặc nhiều Proxy Host.\n\nTính năng này đặc biệt hữu ích đối với:\n\ncác dịch vụ web được forward mà không có cơ chế xác thực tích hợp, hoặc\n\nkhi bạn muốn bảo vệ tài nguyên khỏi những client không xác định.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/Certificates.md",
    "content": "## Hỗ trợ Chứng chỉ\n\n### Chứng chỉ HTTP (HTTP Certificate)\n\nChứng chỉ được xác thực qua HTTP nghĩa là máy chủ của Let's Encrypt sẽ cố gắng truy cập vào tên miền của bạn thông qua HTTP (không phải HTTPS!). Nếu kiểm tra thành công, chứng chỉ sẽ được cấp.\n\nVới phương thức này, bạn phải tạo trước một Proxy Host cho tên miền, có thể truy cập qua HTTP và trỏ về đúng cài đặt Nginx này.\nSau khi chứng chỉ được cấp, bạn có thể chỉnh sửa Proxy Host để sử dụng chứng chỉ đó cho kết nối HTTPS.\n\nTuy nhiên, Proxy Host vẫn phải hỗ trợ truy cập HTTP để việc gia hạn chứng chỉ diễn ra bình thường.\n\nPhương thức này _không hỗ trợ_ wildcard domain.\n\n### Chứng chỉ DNS (DNS Certificate)\n\nChứng chỉ được xác thực qua DNS yêu cầu bạn sử dụng plugin của DNS Provider.\nPlugin này sẽ tạo các bản ghi tạm thời trong DNS của bạn để Let's Encrypt kiểm tra quyền sở hữu tên miền. Nếu hợp lệ, chứng chỉ sẽ được cấp.\n\nKhi dùng phương thức này: Bạn không cần tạo sẵn Proxy Host trước và bạn không cần mở HTTP cho Proxy Host.\n\nPhương thức DNS _có hỗ trợ_ wildcard domain.\n\n### Chứng chỉ tùy chỉnh (Custom Certificate)\n\nTùy chọn này cho phép bạn tải lên chứng chỉ SSL của riêng mình, được cung cấp bởi Certificate Authority (CA) mà bạn tự chọn.\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/DeadHosts.md",
    "content": "## 404 Host là gì?\n\n404 Host đơn giản là một host được thiết lập để hiển thị trang 404.\n\nĐiều này có thể hữu ích khi tên miền của bạn vẫn xuất hiện trên các công cụ tìm kiếm và bạn muốn hiển thị một trang lỗi đẹp hơn, hoặc muốn thông báo rõ ràng cho các trình thu thập dữ liệu tìm kiếm rằng các trang thuộc tên miền đó không còn tồn tại.\n\nMột lợi ích khác của việc có 404 Host là bạn có thể theo dõi nhật ký truy cập vào nó và\nxem các nguồn giới thiệu (referrers)."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md",
    "content": "## Proxy Host là gì?\n\nProxy Host là điểm truy cập đầu vào cho một dịch vụ web mà bạn muốn chuyển tiếp.\n\nNó cung cấp khả năng kết thúc SSL (SSL termination) tùy chọn cho các dịch vụ vốn không hỗ trợ SSL tích hợp.\n\nProxy Host là loại cấu hình phổ biến nhất trong Nginx Proxy Manager."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md",
    "content": "## Redirection Host là gì?\n\nRedirection Host sẽ chuyển hướng các yêu cầu từ tên miền truy cập vào và đưa người xem sang một tên miền khác\n\nLý do phổ biến nhất để sử dụng loại host này là khi trang web của bạn đổi sang tên miền mới nhưng vẫn còn các liên kết từ công cụ tìm kiếm hoặc nguồn giới thiệu trỏ về tên miền cũ."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/Streams.md",
    "content": "## Stream là gì?\n\nStream là một tính năng tương đối mới của Nginx, dùng để chuyển tiếp lưu lượng\nTCP/UDP trực tiếp tới một máy khác trong mạng.\n\nNếu bạn đang vận hành các máy chủ game, FTP hoặc SSH thì tính năng này sẽ rất hữu ích."
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/vi/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/AccessLists.md",
    "content": "## 什么是通信规则？\n \n通信规则提供了一个特定客户IP地址的黑名单或白名单，以及通过基本HTTP认证对代理服务的认证。\n\n你可以为一个通信规则配置多个客户规则、用户名和密码，然后将其应用于代理服务。\n\n这对那些没有内置认证机制的转发网络服务或你想保护其免受未知客户的访问是最有用的。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/Certificates.md",
    "content": "## 证书帮助\n\n### HTTP 证书\n\nHTTP 验证的证书表示 Let's Encrypt 服务器将尝试通过 HTTP（而非 HTTPS！）访问您的域名，如果成功，它们将为您颁发证书。\n\n使用此方法时，您必须为您的域名创建一个可通过 HTTP 访问并指向此 Nginx 安装的 代理主机。在获得证书后，您可以修改该 代理主机，使其也使用此证书处理 HTTPS 连接。然而，为了证书能够续期，该 代理主机 仍需配置为支持 HTTP 访问。\n\n此过程_不支持_通配符域名。\n\n### DNS 证书\n\nDNS 验证的证书要求您使用一个 DNS 服务商插件。该 DNS 服务商将用于在您的域名下创建临时记录，随后 Let's Encrypt 将查询这些记录以确认您是域名所有者，如果成功，它们将为您颁发证书。\n\n请求此类证书前，您无需预先创建 代理主机，也无需将您的 代理主机 配置为支持 HTTP 访问。\n\n此过程_支持_通配符域名。\n\n### 自定义证书\n\n使用此选项上传您自己的 SSL 证书，该证书由您自己的证书颁发机构提供。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/DeadHosts.md",
    "content": "## 什么是错误页面？\n \n错误页面是一个简单的主机设置，显示错误页面。\n\n当你的域名被列入搜索引擎，而你想提供一个更好的错误页面或特别是告诉搜索索引者域名页面不再存在时，这可能是有用的。\n\n拥有这种主机的另一个好处是可以跟踪点击它的日志并查看访问来源。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/ProxyHosts.md",
    "content": "## 什么是代理服务？\n \n代理服务是你想转发网络应用的主机。\n\n代理服务可以为没有SSL服务的网络应用提供SSL服务（可选）。\n\n代理服务是Nginx代理管理器的最常见用途之一。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/RedirectionHosts.md",
    "content": "## 什么是重定向？\n \n重定向是将接入域名的请求推送到另一个域名。\n\n使用这种类型的主机最常见的原因是当你的网站改变了域名，但你仍然有链接指向旧域名的应用。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/Streams.md",
    "content": "## 什么是端口转发？\n \n端口转发是Nginx的一个相对较新的功能，可以直接转发 TCP/UDP 流量到网络上的另一台计算机。\n\n如果你正在运行游戏服务器、FTP或SSH服务器，这个功能就会很有用。\n"
  },
  {
    "path": "frontend/src/locale/src/HelpDoc/zh/index.ts",
    "content": "export * as AccessLists from \"./AccessLists.md\";\nexport * as Certificates from \"./Certificates.md\";\nexport * as DeadHosts from \"./DeadHosts.md\";\nexport * as ProxyHosts from \"./ProxyHosts.md\";\nexport * as RedirectionHosts from \"./RedirectionHosts.md\";\nexport * as Streams from \"./Streams.md\";\n"
  },
  {
    "path": "frontend/src/locale/src/bg.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Списък за достъп\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {правило} other {правила}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {потребител} other {потребители}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Когато съществува поне 1 правило, това правило за отказ се добавя последно\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Обърнете внимание, че правилата Позволяване и Отказване се прилагат в реда, в който са зададени.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Предаване на автентикация към Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Публичен достъп\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Без базова автентикация\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 или 192.168.1.0/24 или 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Удовлетворяване на което и да е\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {потребител} other {потребители}}, {rules} {rules, plural, one {правило} other {правила}} - Създадено: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Списъци за достъп\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Добавяне\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Добавяне на маршрут\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Разрешаване\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Затваряне\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Изтриване\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Отказване\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Деактивиране\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Изтегляне\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Редактиране\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Активиране\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Права\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Подновяване\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Преглед на детайли\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Журнали за одит\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Автоматично\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Отказ\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Сертификат\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Сертификат\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Ключ на сертификата\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Междинен сертификат\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Използва се\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Не е назначен сертификат\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Този хост няма да използва HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Без сертификат\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Не се използва\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Подновяване на сертификат\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Сертификати\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Потребителски сертификат\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Ключове, защитени с парола, не се поддържат.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Съдържание на файл с удостоверения\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Този плъгин изисква конфигурационен файл с API токен или други идентификационни данни.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Тези данни ще бъдат съхранени като обикновен текст в базата и във файл!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Секунди за разпространение\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Оставете празно, за да се използва стойността по подразбиране. Брой секунди за изчакване на DNS разпространение.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS доставчик\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Изберете доставчик...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Неуспешна проверка поради грешка в комуникацията със site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Няма достъпен сървър на този домейн. Проверете, че домейнът съществува и сочи към IP-та, където се изпълнява NPM, и ако е необходимо, че порт 80 е пренасочен.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Вашият сървър е достъпен и създаването на сертификати е възможно.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Намерен е сървър, но върна неочакван код {code}. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Намерен е сървър, но върна неочаквани данни. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Резултати от теста\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Тези домейни трябва вече да сочат към тази инсталация.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Тип ключ\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA е широко съвместим, ECDSA е по-бърз и по-сигурен, но може да не се поддържа от по-стари системи\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"с Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Заявка за нов сертификат\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Достъп\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Автентикация\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Автентикации\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Персонализирани маршрути\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Дестинация\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Детайли\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Имейл\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Събитие\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Изтича\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP код\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Входящ порт\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Име\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Протокол\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Доставчик\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Роли\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Правила\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Удовлетворяване\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Всички\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Кое и да е\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Схема\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Източник\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Статус\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Създадено: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Табло\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 хост\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 хостове\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 хост} other {404 хостове}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Деактивиран\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Домейн имена\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Максимум {count} домейна\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Започнете да въвеждате, за да добавите домейн...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcard не е разрешен за този тип\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcard не се поддържа от това CA\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Принудително SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS активирано\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS за поддомейни\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Поддръжка на HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Използване на DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Имейл адрес\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Няма резултати\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Защо не създадете един?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Активиран\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Необходимо е поне една Автентикация или едно Правило за достъп\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Потребителските имена за достъп трябва да са уникални\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Невалиден имейл или парола\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Невалиден домейн: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Невалиден имейл адрес\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Максималната дължина е {max} знак{max, plural, one {} other {а}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Твърде много домейни, максимум {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Максимум {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Минималната дължина е {min} знак{min, plural, one {} other {а}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Минимум e {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Паролите трябва да съвпадат\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Това поле е задължително\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Изтича: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork в GitHub\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Блокиране на често срещани експлойти\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Кеширане на ресурси\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Запазване на пътя\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Протоколи\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Поддръжка на WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Порт\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Схема\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Хостове\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Само HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt чрез DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt чрез HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Зареждане…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Вход в акаунта\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Персонализирана Nginx конфигурация\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Въведете вашата персонализирана Nginx конфигурация на собствен риск!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Нямате достъп до тази страница.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Към началната страница\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Страницата, която търсите, не беше намерена\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Упс… Намерихте грешка\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Грешка\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} беше изтрит\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} беше деактивиран\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} беше активиран\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} беше подновен\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} беше запазен\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Успех\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} №{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Добавяне: {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Изтриване: {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Сигурни ли сте, че искате да изтриете {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Редактиране: {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Няма налични {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Създаден {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Изтрит {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Деактивиран {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Активиран {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Подновен {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Актуализиран {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Офлайн\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Онлайн\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Опции\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Парола\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Генериране на случайна парола\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Скриване на паролата\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Показване на паролата\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Скрито\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Управление\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Само преглед\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Всички елементи\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Видимост на елементите\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Само създадените от потребителя\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Прокси хост\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Хост/IP за препращане\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Прокси хостове\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {прокси хост} other {прокси хостове}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Публичен\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Хост за пренасочване\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Домейн за пренасочване\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP код\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Хостове за пренасочване\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {хост за пренасочване} other {хостове за пренасочване}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiple Choices\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Преместено постоянно\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Преместено временно\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 See other\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Временно пренасочване\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Постоянно пренасочване\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Администратор\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Обикновен потребител\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Запазване\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Настройка\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Настройки\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Сайт по подразбиране\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 страница\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Без отговор (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Страница поздравление\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Какво да се показва при заявка към неизвестен хост\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Персонализиран HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Въведете вашето персонализирано HTML съдържание тук -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Пренасочване\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Започнете, като създадете администраторски акаунт.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Добре дошли!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Вход\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL сертификат\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Поток\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Хост за препращане\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com или 10.0.0.1 или 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Входящ порт\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Потоци\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {поток} other {потоци}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Тест\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Налична актуализация: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Потребител\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Смяна на парола\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Потвърждение на парола\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Текуща парола\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Редактиране на профил\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Пълно име\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Вход като {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Изход\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Нова парола\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Псевдоним\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Задаване на парола\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Настройка на права за {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Тъмна тема\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Светла тема\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Потребителско име\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Потребители\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/cs.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Počet zbývajících záložních kódů: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Tyto záložní kódy si uložte na bezpečném místě. Každý kód lze použít pouze jednou.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Vypnout dvoufaktorové ověřování\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"Vypnout 2FA\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"Vypnutím dvoufaktorového ověřování snížíte bezpečnost svého účtu.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Vypnuto\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"Uložil jsem si své záložní kódy.\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Zapnout dvoufaktorové ověřování\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Zapnuto\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Zadejte ověřovací kód\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Zadejte ověřovací kód pro vypnutí\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Znovu vytvořit\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Znovu vytvořit záložní kódy\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Zadejte ověřovací kód pro vytvoření nových záložních kódů. Vaše staré kódy budou neplatné.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Tajný klíč\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Naskenujte tento QR kód pomocí své ověřovací aplikace nebo zadejte tajný klíč ručně.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Stav\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Dvoufaktorové ověření\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Ověřit a zapnout\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"seznam přístupů\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {pravidlo} few {pravidla} other {pravidel}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {uživatel} few {uživatelé} other {uživatelů}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Když existuje alespoň jedno pravidlo, toto pravidlo „zamítnout vše“ bude přidáno jako poslední\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Upozornění: pravidla povolit a zamítnout budou uplatňována v pořadí, v jakém jsou definována.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Odeslat ověření na Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Veřejně přístupné\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Není potřeba základní ověření\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 nebo 192.168.1.0/24 nebo 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Splnit kterékoliv\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {uživatel} few {uživatelé} other {uživatelů}}, {rules} {rules, plural, one {pravidlo} few {pravidla} other {pravidel}} - Vytvořeno: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Seznamy přístupů\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Přidat\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Přidat umístění\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Povolit\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Zavřít\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Smazat\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Zamítnout\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Deaktivovat\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Stáhnout\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Upravit\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aktivovat\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Oprávnění\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Obnovit\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Zobrazit podrobnosti\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Záznamy auditu\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Automaticky\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Zrušit\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"certifikát\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certifikát\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Klíč certifikátu\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Zprostředkovatelský certifikát\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Používá se\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Není přiřazen žádný certifikát\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Tento hostitel nebude používat HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Žádný\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Nepoužívá se\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Obnovit certifikát\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certifikáty\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Vlastní certifikát\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Soubory klíčů chráněné heslem nejsou podporovány.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Obsah souboru s přihlašovacími údaji\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Tento doplněk vyžaduje konfigurační soubor obsahující API token nebo jiné přihlašovací údaje vašeho poskytovatele\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Tyto údaje budou uloženy v databázi a v souboru jako obyčejný text!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagace v sekundách\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Nechte prázdné pro výchozí hodnotu doplňku. Počet sekund, po které se čeká na propagaci DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS poskytovatel\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Vyberte poskytovatele...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Tato sekce vyžaduje znalost Certbotu a jeho DNS doplňků. Prosím, podívejte se do dokumentace příslušného doplňku.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Na této doméně byl nalezen server, ale nezdá se, že jde o Nginx Proxy Manager. Ujistěte se, že vaše doména směřuje na IP, kde běží vaše instance NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Na této doméně není dostupný žádný server. Ujistěte se, že doména existuje a směřuje na IP adresu s NPM a pokud je to potřeba, port 80 je přesměrován ve vašem routeru.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Váš server je dostupný a vytvoření certifikátu by mělo být možné.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Na této doméně byl nalezen server, ale vrátil neočekávaný stavový kód {code}. Je to NPM server? Ujistěte se prosím, že doména směřuje na IP, kde běží vaše instance NPM.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Na této doméně byl nalezen server, ale vrátil neočekávaná data. Je to NPM server? Ujistěte se, že doména směřuje na IP, kde běží vaše instance NPM.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Výsledky testu\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Tyto domény musí být již nakonfigurovány tak, aby směřovaly na tuto instalaci.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Typ klíče\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA je široce kompatibilní, ECDSA je rychlejší a bezpečnější, ale nemusí být podporován staršími systémy\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"pomocí Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Vyžádat nový certifikát\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Přístup\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorizace\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorizace\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Vlastní umístění\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Cíl\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Podrobnosti\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Událost\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Platnost do\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Přístup\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Vstupní port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Název\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Poskytovatel\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Role\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Pravidla\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Splnit\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Všechny\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Kterékoliv\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Zdroj\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Stav\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Vytvořeno: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Panel\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 hostitel\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hostitelé\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 hostitel} few {404 hostitelé} other {404 hostitelů}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Deaktivováno\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Doménová jména\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maximálně {count} doménových jmen\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Začněte psát pro přidání domény...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards nejsou pro tento typ povoleny\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards nejsou podporovány pro tuto certifikační autoritu\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Vynutit SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS povoleno\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS pro subdomény\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Podpora HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Použít DNS výzvu\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Emailová adresa\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nebyly nalezeny žádné výsledky\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Proč nevytvoříte nějaký?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Aktivováno\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Je vyžadována alespoň jedna autorizace nebo jedno přístupové pravidlo\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Uživatelská jména pro autorizaci musí být jedinečná\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Neplatný email nebo heslo\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Neplatná doména: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Neplatná emailová adresa\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximální délka je {max} znak{max, plural, one {} few {y} other {ů}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Příliš mnoho domén, maximum je {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximum je {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimální délka je {min} znak{min, plural, one {} few {y} other {ů}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum je {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Hesla se musí shodovat\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Toto pole je povinné\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Platnost do: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forkněte mě na GitHubu\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokovat běžné exploity\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Uložit zdroje do mezipaměti\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Zachovat cestu\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokoly\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Podpora WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port přesměrování\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hostitelé\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Pouze HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt přes DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt přes HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Načítá se…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Ověřovací kód\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Vložit kód\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Vložte kód z vaší ověřovací aplikace\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"Dvoufaktorové ověření\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Ověřit\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Přihlaste se ke svému účtu\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Vlastní Nginx konfigurace\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Zadejte vlastní Nginx konfiguraci na vlastní riziko!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Nemáte oprávnění k zobrazení tohoto obsahu.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Zpět na hlavní stránku\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Omlouváme se, stránka, kterou hledáte, nebyla nalezena\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Ups… Našli jste chybovou stránku\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Chyba\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} byl odstraněn\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} byl deaktivován\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} byl aktivován\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} byl obnoven\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} byl uložen\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Úspěch\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Přidat {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Smazat {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Opravdu chcete smazat tento {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Upravit {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Nejsou {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Vytvořen {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Smazán {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Deaktivován {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Aktivován {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Obnoven {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Aktualizován {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Možnosti\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Heslo\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Vygenerovat náhodné heslo\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Skrýt heslo\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Zobrazit heslo\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Skryté\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Spravovat\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Pouze pro zobrazení\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Všechny položky\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Viditelnost položky\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Pouze vytvořené položky\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"proxy hostitele\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Cílový název hostitele / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy hostitelé\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {proxy hostitel} few {proxy hostitelé} other {proxy hostitelů}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Veřejné\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"přesměrovacího hostitele\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Cílová doména\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP kód\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Přesměrovací hostitelé\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {přesměrovací hostitel} few {přesměrovací hostitelé} other {přesměrovacích hostitelů}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Více možností\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Trvale přesunuto\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Dočasně přesunuto\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Podívat se na jiné\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Dočasné přesměrování\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Trvalé přesměrování\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrátor\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Běžný uživatel\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Uložit\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Nastavení\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Nastavení\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Výchozí stránka\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Stránka 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Bez odpovědi (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Gratulační stránka\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Co zobrazit, když Nginx zachytí neznámého hostitele\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Vlastní HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Sem zadejte vlastní HTML obsah -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Přesměrovat\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Začněte vytvořením administrátorského účtu.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Vítejte!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Přihlásit se\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL certifikát\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Cílový hostitel\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"napriklad.cz nebo 10.0.0.1 nebo 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Vstupní port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streamy\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {stream} few {streamy} other {streamů}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Dostupná aktualizace: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"uživatele\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Změnit heslo\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Potvrdit heslo\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Aktuální heslo\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Upravit profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Celé jméno\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Přihlásit se jako {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Odhlásit se\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nové heslo\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Přezdívka\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Nastavit heslo\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Nastavit oprávnění pro {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Přepnout na tmavý režim\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Přepnout na světlý režim\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"Dvoufaktorové ověření\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Uživatelské jméno\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Uživatelé\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/de.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Zugriffsliste\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Regel} other {Regeln}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {User} other {Users}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt.\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Authentifizierung an Upstream weiterleiten\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Öffentlich\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Keine Authentifizierung erforderlich\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Satisfy Any\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Zugriffslisten\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Hinzufügen\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Pfad hinzufügen\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Schließen\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Löschen\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Deaktivieren\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Herunterladen\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Bearbeiten\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aktivieren\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Berechtigungen\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Erneuern\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Details anzeigen\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Protokolle\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Abbrechen\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Zertifikat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Zertifikat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Privater Schlüssel\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Zwischenzertifikat\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Benutzung\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Kein Zertifikat zugewiesen\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Dieser Host verwendet kein HTTPS.\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Kein\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Nicht in Benutzung\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Zertifikat erneuern\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Zertifikate\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Benutzerdefiniertes Zertifikat\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Inhalt der Anmeldedaten-Datei\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Dieses Plugin erfordert eine Konfigurationsdatei, die einen API-Token oder andere Anmeldedaten für Ihren Anbieter enthält.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Diese Daten werden als Klartext in der Datenbank und in einer Datei gespeichert!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Wartezeit in Sekunden\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Leer lassen um die Standardwartezeit des Plugins zu nutzen\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS Provider\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um Nginx Proxy Manager zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Unter dieser Domain ist kein Server verfügbar. Bitte stellen Sie sicher, dass Ihre Domain existiert und auf die IP-Adresse verweist, unter der Ihre NPM-Instanz läuft, und dass gegebenenfalls Port 80 in Ihrem Router weitergeleitet wird.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Ihr Server ist erreichbar und die Erstellung von Zertifikaten sollte möglich sein.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Unter dieser Domain wurde ein Server gefunden, der jedoch einen unerwarteten Statuscode {code} zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Unter dieser Domain wurde ein Server gefunden, der jedoch unerwartete Daten zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Testergebnisse\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Schlüsseltyp\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA ist weit verbreitet, ECDSA ist schneller und sicherer, wird aber möglicherweise von älteren Systemen nicht unterstützt\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"Über Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Anfordern eines neuen Zertifikates\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Zugriff\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Genehmigung\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Genehmigungen\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Benutzerdefinierte Pfade\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Ziel\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Details\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"E-Mail\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Ereignis\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Verfällt am\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Eingehender Port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Name\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokoll\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Provider\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Rollen\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Regeln\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfy\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Alle\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Jeder\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Quelle\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Erstelldatum: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Host\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hosts\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Host} other {404 Hosts}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Deaktiviert\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domain Names\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} Maximale Anzahl von Domainnamen\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Eintragen der Domain...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards sind für diesen Typ nicht zulässig.\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards werden für diese Zertifizierungsstelle nicht unterstützt.\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Erzwinge SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS aktiviert\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Sub-domains\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Support\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Nutze DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"E-Mail-Adresse\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Keine Ergebnisse gefunden\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Warum erstellen Sie nicht eine?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"aktiviert\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich.\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Autorisierung Benutzernamen müssen eindeutig sein\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Ungültige E-Mail-Adresse oder Passwort\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Ungültige Domain: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Ungültige E-Mail-Adresse\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Zu viele Domains, maximal sind {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximum ist {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Die minimale Länge beträgt {min} Zeichen{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum ist {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Passwörter müssen übereinstimmen\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Dies ist erforderlich.\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Ablauf am: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork me on Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Gängige Exploits blockieren\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache Assets\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Pfad beibehalten\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokole\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets Support\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Forward Port\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Only\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Laden…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Anmelden\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Benutzerdefinierte Nginx Konfiguration\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Sie haben keinen Zugriff, um dies anzuzeigen.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Take me home\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Es tut uns leid, aber die gesuchte Seite wurde nicht gefunden\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… You just found an error page\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Error\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} wurde gelöscht\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} wurde deaktiviert\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} wurde aktiviert\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} wurde erneuert\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} wurde gespeichert\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Erfolgreich\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"{object} hinzufügen\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"{object} löschen\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"{object} wirklich löschen?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"{object} bearbeiten\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Keine {objects} vorhanden\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} erstellt\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} gelöscht\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} deaktiviert\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} aktiviert\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} erneuert\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} aktualisiert\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Optionen\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Passwort\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Zufälliges Passwort generieren\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Passwort verstecken\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Passwort anzeigen\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Versteckt\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Verwalten\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Nur anzeigen\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Alle Elemente\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Objektsichtbarkeit\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Nur erstellte Elemente\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Öffentlich\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Redirection Host\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Forward Domain\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Redirection Hosts\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standardbenutzer\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Speichern\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Einstellung\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Einstellungen\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Standardseite\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 Page\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"No Response (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Willkommensseite\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Was angezeigt wird, wenn der Nginx eine unbekannte Webseitenanfrage bekommt\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Benutzerdefinierte HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Geben Sie hier Ihren benutzerdefinierten HTML-Inhalt ein. -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Weiterleitung\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Beginnen Sie mit der Erstellung Ihres Administratorkontos.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Willkommen!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Login\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL-Zertifikate\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Host\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Incoming Port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"User\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Passwort ändern\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Passwort wiederholen\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Aktuelles Passwort\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Profil bearbeiten\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Name\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Einloggen als {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Ausloggen\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Neues Password\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Nickname\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Passwort setzen\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Berechtigungen für {name} setzen\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Zum Dark Mode wechseln\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Zum Light Mode wechseln\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Benutzername\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Benutzer\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/en.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Backup codes remaining: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Save these backup codes in a secure place. Each code can only be used once.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Disable Two-Factor Authentication\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"Disable 2FA\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"Disabling two-factor authentication will make your account less secure.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Disabled\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"I have saved my backup codes\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Enable Two-Factor Authentication\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Enabled\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Enter verification code\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Enter verification code to disable\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Regenerate\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Regenerate Backup Codes\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Enter a verification code to generate new backup codes. Your old codes will be invalidated.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Secret Key\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Scan this QR code with your authenticator app, or enter the secret manually.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Two-Factor Authentication\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Verify and Enable\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Access List\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Rule} other {Rules}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {User} other {Users}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"When at least 1 rule exists, this deny all rule will be added last\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Note that the allow and deny directives will be applied in the order they are defined.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Pass Auth to Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Publicly Accessible\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"No basic auth required\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Satisfy Any\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Access Lists\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Add\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Add Location\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Allow\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Close\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Delete\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Deny\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Disable\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Download\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Edit\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Enable\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permissions\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Renew\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"View Details\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Audit Logs\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Auto\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Cancel\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificate\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificate\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Certificate Key\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Intermediate Certificate\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Use\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"No certificate assigned\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"This host will not use HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"None\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Not Used\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Renew Certificate\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificates\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Custom Certificate\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Key files protected with a passphrase are not supported.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Credentials File Content\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"This plugin requires a configuration file containing an API token or other credentials for your provider\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"This data will be stored as plaintext in the database and in a file!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagation Seconds\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS Provider\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Select a Provider...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Failed to check the reachability due to a communication error with site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Your server is reachable and creating certificates should be possible.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"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.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Test Results\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"These domains must be already configured to point to this installation.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Key Type\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"with Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Request a new Certificate\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Access\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Authorization\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Authorizations\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Custom Locations\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destination\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Details\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Event\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Expires\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Incoming Port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Name\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Provider\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roles\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Rules\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfy\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"All\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Any\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Source\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Created: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Host\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hosts\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Host} other {404 Hosts}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Disabled\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domain Names\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} domain names maximum\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Start typing to add domain...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards not permitted for this type\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards not supported for this CA\"\n\t},\n\t\"domains.advanced\": {\n\t\t\"defaultMessage\": \"Advanced\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Force SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Enabled\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Sub-domains\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Support\"\n\t},\n\t\"domains.trust-forwarded-proto\": {\n\t\t\"defaultMessage\": \"Trust Upstream Forwarded Proto Headers\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Use DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Email address\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"No results found\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Why don't you create one?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Enabled\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Either one Authorization or one Access Rule is required\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Authorization Usernames must be unique\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Invalid email or password\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Invalid domain: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Invalid email address\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximum length is {max} character{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Too many domains, max is {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximum is {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimum length is {min} character{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum is {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Passwords must match\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"This is required\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Expires: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork me on Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Block Common Exploits\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache Assets\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Preserve Path\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocols\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets Support\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Forward Port\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Only\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Loading…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Verification Code\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Enter code\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Enter the code from your authenticator app\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"Two-Factor Authentication\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Verify\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Login to your account\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Custom Nginx Configuration\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Enter your custom Nginx configuration here at your own risk!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"You do not have access to view this.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Take me home\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"We are sorry but the page you are looking for was not found\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… You just found an error page\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Error\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} has been deleted\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} has been disabled\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} has been enabled\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} has been renewed\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} has been saved\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Success\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Add {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Delete {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Are you sure you want to delete this {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Edit {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"There are no {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Created {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Deleted {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Disabled {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Enabled {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Renewed {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Updated {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Options\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Password\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Generate random password\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Hide Password\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Show Password\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Hidden\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Manage\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"View Only\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"All Items\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Item Visibility\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Created Items Only\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Public\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Redirection Host\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Forward Domain\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Redirection Hosts\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiple Choices\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Moved permanently\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Moved temporarily\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 See other\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Temporary redirect\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Permanent redirect\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standard User\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Save\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Setting\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Settings\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Default Site\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 Page\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"No Response (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Congratulations Page\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"What to show when Nginx is hit with an unknown Host\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Custom HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Enter your custom HTML content here -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Redirect\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Get started by creating your admin account.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Welcome!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Sign in\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL Certificate\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Host\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Incoming Port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Update Available: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"User\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Change Password\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Confirm Password\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Current Password\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Edit Profile\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Full Name\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Sign in as {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Logout\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"New Password\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Nickname\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Set Password\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Set Permissions for {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Switch to Dark mode\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Switch to Light mode\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"Two-Factor Auth\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Username\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Users\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/es.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Lista de Acceso\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Regla} other {Reglas}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Usuario} other {Usuarios}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Cuando exista al menos 1 regla, esta regla de denegar todo se añadirá al final\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Ten en cuenta que las directivas de permitir y denegar se aplicarán en el orden en que estén definidas.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Pasar Autenticación al Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Accesible Públicamente\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"No se requiere autenticación básica\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 o 192.168.1.0/24 o 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Satisfacer Cualquiera\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Usuario} other {Usuarios}}, {rules} {rules, plural, one {Regla} other {Reglas}} - Creado: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Listas de Acceso\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Añadir\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Añadir Ubicación\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Permitir\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Cerrar\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Eliminar\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Denegar\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Deshabilitar\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Descargar\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Editar\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Habilitar\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permisos\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Renovar\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Ver Detalles\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Registros de Auditoría\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Auto\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Cancelar\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificado\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificado\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Clave del Certificado\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Certificado Intermedio\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"En Uso\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Sin certificado asignado\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Este host no usará HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Ninguno\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Sin Usar\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Renovar Certificado\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificados\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Certificado Personalizado\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"No se admiten archivos de claves protegidos con contraseña.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Contenido del Archivo de Credenciales\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Este plugin requiere un archivo de configuración que contenga un token de API u otras credenciales para tu proveedor\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"¡Estos datos se almacenarán como texto plano en la base de datos y en un archivo!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Segundos de Propagación\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Dejar vacío para usar el valor predeterminado del plugin. Número de segundos a esperar para la propagación DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Proveedor DNS\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Selecciona un Proveedor...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Esta sección requiere algunos conocimientos sobre Certbot y sus plugins DNS. Consulta la documentación de los plugins respectivos.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Se encontró un servidor en este dominio pero no parece ser Nginx Proxy Manager. Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"No hay ningún servidor disponible en este dominio. Asegúrate de que tu dominio existe y apunta a la IP donde se está ejecutando tu instancia de NPM y, si es necesario, que el puerto 80 esté redirigido en tu router.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Tu servidor es accesible y debería ser posible crear certificados.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Se encontró un servidor en este dominio pero devolvió un código de estado inesperado {code}. ¿Es el servidor NPM? Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Se encontró un servidor en este dominio pero devolvió datos inesperados. ¿Es el servidor NPM? Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Resultados de la Prueba\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Estos dominios ya deben estar configurados para apuntar a esta instalación.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Tipo de Clave\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA es ampliamente compatible, ECDSA es más rápido y seguro pero puede no ser compatible con sistemas antiguos\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"con Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Solicitar un nuevo Certificado\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Acceso\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorización\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorizaciones\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Ubicaciones Personalizadas\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destino\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Detalles\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Correo Electrónico\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Evento\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Expira\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Código HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Puerto de Entrada\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nombre\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocolo\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Proveedor\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roles\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Reglas\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfacer\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Todo\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Cualquiera\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Esquema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Origen\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Estado\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Creado: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Panel de Control\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Host 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Hosts 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host 404} other {Hosts 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Deshabilitado\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Nombres de Dominio\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} nombres de dominio como máximo\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Comienza a escribir para añadir dominio...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"No se permiten comodines para este tipo\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"No se admiten comodines para esta CA\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Forzar SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Habilitado\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS en Subdominios\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Soporte HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Usar Desafío DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Dirección de correo electrónico\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"No se encontraron resultados\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"¿Por qué no creas uno?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Habilitado\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Se requiere al menos una Autorización o una Regla de Acceso\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Los nombres de usuario de autorización deben ser únicos\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Correo electrónico o contraseña no válidos\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Dominio no válido: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Dirección de correo electrónico no válida\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"La longitud máxima es {max} caracter{max, plural, one {} other {es}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Demasiados dominios, el máximo es {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"El máximo es {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"La longitud mínima es {min} caracter{min, plural, one {} other {es}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"El mínimo es {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Las contraseñas deben coincidir\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Este campo es obligatorio\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Expira: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Bifúrcame en Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Bloquear Exploits Comunes\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cachear Recursos\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Preservar Ruta\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocolos\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Soporte de Websockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Puerto\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Esquema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Solo HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt vía DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt vía HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Cargando…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Inicia sesión en tu cuenta\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Configuración Personalizada de Nginx\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# ¡Introduce aquí tu configuración personalizada de Nginx bajo tu propio riesgo!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"No tienes acceso para ver esto.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Llévame al inicio\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Lo sentimos, pero la página que buscas no fue encontrada\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Ups… Has encontrado una página de error\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Error\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} ha sido eliminado\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} ha sido deshabilitado\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} ha sido habilitado\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} ha sido renovado\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} ha sido guardado\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Éxito\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Añadir {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Eliminar {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"¿Estás seguro de que quieres eliminar este {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Editar {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"No hay {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} Creado\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} Eliminado\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} Deshabilitado\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} Habilitado\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} Renovado\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} Actualizado\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Desconectado\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Conectado\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opciones\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Contraseña\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Generar contraseña aleatoria\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Ocultar Contraseña\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Mostrar Contraseña\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Oculto\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Gestionar\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Solo Ver\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Todos los Elementos\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Visibilidad de Elementos\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Solo Elementos Creados\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Host Proxy\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Nombre de Host / IP de Reenvío\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Hosts Proxy\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host Proxy} other {Hosts Proxy}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Público\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Host de Redirección\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Dominio de Reenvío\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Código HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Hosts de Redirección\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host de Redirección} other {Hosts de Redirección}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiples Opciones\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Movido permanentemente\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Movido temporalmente\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Ver otro\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Redirección temporal\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Redirección permanente\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrador\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Usuario Estándar\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Guardar\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Configuración\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Configuración\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Sitio Predeterminado\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Página 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Sin Respuesta (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Página de Felicitaciones\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Qué mostrar cuando Nginx recibe un Host desconocido\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML Personalizado\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Introduce aquí tu contenido HTML personalizado -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Redirigir\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Comienza creando tu cuenta de administrador.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"¡Bienvenido!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Iniciar Sesión\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Certificado SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Host de Reenvío\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com o 10.0.0.1 o 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Puerto de Entrada\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Probar\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Usuario\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Cambiar Contraseña\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Confirmar Contraseña\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Contraseña Actual\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Editar Perfil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Nombre Completo\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Iniciar sesión como {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Cerrar Sesión\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nueva Contraseña\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Apodo\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Establecer Contraseña\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Establecer Permisos para {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Cambiar a modo Oscuro\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Cambiar a modo Claro\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nombre de Usuario\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Usuarios\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/et.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Backup codes remaining: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Save these backup codes in a secure place. Each code can only be used once.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Disable Two-Factor Authentication\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"Disable 2FA\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"Disabling two-factor authentication will make your account less secure.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Disabled\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"I have saved my backup codes\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Enable Two-Factor Authentication\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Enabled\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Enter verification code\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Enter verification code to disable\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Regenerate\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Regenerate Backup Codes\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Enter a verification code to generate new backup codes. Your old codes will be invalidated.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Secret Key\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Scan this QR code with your authenticator app, or enter the secret manually.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Two-Factor Authentication\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Verify and Enable\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Access List\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Rule} other {Rules}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {User} other {Users}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"When at least 1 rule exists, this deny all rule will be added last\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Note that the allow and deny directives will be applied in the order they are defined.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Pass Auth to Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Publicly Accessible\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"No basic auth required\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Satisfy Any\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Access Lists\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Add\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Add Location\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Allow\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Close\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Delete\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Deny\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Disable\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Download\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Edit\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Enable\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permissions\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Renew\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"View Details\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Audit Logs\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Auto\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Cancel\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificate\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificate\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Certificate Key\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Intermediate Certificate\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Use\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"No certificate assigned\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"This host will not use HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"None\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Not Used\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Renew Certificate\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificates\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Custom Certificate\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Key files protected with a passphrase are not supported.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Credentials File Content\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"This plugin requires a configuration file containing an API token or other credentials for your provider\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"This data will be stored as plaintext in the database and in a file!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagation Seconds\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS Provider\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Select a Provider...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Failed to check the reachability due to a communication error with site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Your server is reachable and creating certificates should be possible.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"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.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Test Results\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"These domains must be already configured to point to this installation.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Key Type\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"with Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Request a new Certificate\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Access\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Authorization\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Authorizations\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Custom Locations\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destination\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Details\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Event\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Expires\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Incoming Port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Name\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Provider\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roles\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Rules\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfy\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"All\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Any\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Source\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Created: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Host\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hosts\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Host} other {404 Hosts}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Disabled\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domain Names\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} domain names maximum\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Start typing to add domain...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards not permitted for this type\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards not supported for this CA\"\n\t},\n\t\"domains.advanced\": {\n\t\t\"defaultMessage\": \"Advanced\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Force SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Enabled\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Sub-domains\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Support\"\n\t},\n\t\"domains.trust-forwarded-proto\": {\n\t\t\"defaultMessage\": \"Trust Upstream Forwarded Proto Headers\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Use DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Email address\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"No results found\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Why don't you create one?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Enabled\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Either one Authorization or one Access Rule is required\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Authorization Usernames must be unique\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Invalid email or password\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Invalid domain: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Invalid email address\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximum length is {max} character{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Too many domains, max is {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximum is {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimum length is {min} character{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum is {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Passwords must match\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"This is required\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Expires: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork me on Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Block Common Exploits\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache Assets\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Preserve Path\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocols\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets Support\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Forward Port\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Only\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Loading…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Verification Code\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Enter code\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Enter the code from your authenticator app\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"Two-Factor Authentication\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Verify\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Login to your account\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Custom Nginx Configuration\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Enter your custom Nginx configuration here at your own risk!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"You do not have access to view this.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Take me home\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"We are sorry but the page you are looking for was not found\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… You just found an error page\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Error\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} has been deleted\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} has been disabled\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} has been enabled\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} has been renewed\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} has been saved\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Success\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Add {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Delete {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Are you sure you want to delete this {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Edit {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"There are no {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Created {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Deleted {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Disabled {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Enabled {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Renewed {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Updated {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Options\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Password\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Generate random password\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Hide Password\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Show Password\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Hidden\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Manage\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"View Only\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"All Items\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Item Visibility\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Created Items Only\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Public\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Redirection Host\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Forward Domain\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Redirection Hosts\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiple Choices\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Moved permanently\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Moved temporarily\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 See other\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Temporary redirect\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Permanent redirect\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standard User\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Save\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Setting\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Settings\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Default Site\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 Page\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"No Response (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Congratulations Page\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"What to show when Nginx is hit with an unknown Host\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Custom HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Enter your custom HTML content here -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Redirect\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Get started by creating your admin account.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Welcome!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Sign in\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL Certificate\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Host\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Incoming Port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Update Available: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"User\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Change Password\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Confirm Password\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Current Password\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Edit Profile\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Full Name\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Sign in as {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Logout\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"New Password\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Nickname\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Set Password\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Set Permissions for {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Switch to Dark mode\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Switch to Light mode\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"Two-Factor Auth\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Username\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Users\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/fr.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Liste d'accès\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Règle} other {Règles}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Utilisateur} other {Utilisateurs}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier.\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Transmettre l'authentification au serveur en amont\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Accessible au public\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Aucune authentification de base requise\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Valide n'importe quelle règle\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Listes d'accès\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Ajouter\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Ajouter localisation\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Fermer\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Supprimer\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Désactiver\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Télécharger\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Modifier\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Activer\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permissions\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Renouveler\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Voir les Détails\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Journaux d'audit\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Annuler\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Clé du Certificat\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Certificat intermédiaire\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Utilisé\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Aucun certificat assigné\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Cet hôte n'utilisera pas le HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Aucun\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Non utilisé\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Renouveler Certificat\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificats\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Certificat personnalisé\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Les fichiers de clé protégés par une passphrase ne sont pas acceptés.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Contenu du fichier d'identifiants\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Ces données seront stockées en clair dans la base de données et dans un fichier !\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagation Seconds\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Fournisseur DNS\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Votre serveur est accessible et la création de certificats devrait être possible.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Résultats du test\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Ces domaines doivent déjà être configurés pour pointer vers cette installation.\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"avec Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Demander un nouveau certificat\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Accès\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorisation\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorisations\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Emplacement personnalisé\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destination\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Détails\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"eMail\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Évènement\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Expire\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Code HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Port entrant\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nom\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocole\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Fournisseur\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Rôles\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Règles\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Valide\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"All\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Any\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Source\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Statut\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Créé : {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Tableau de bord\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Hôte 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Hôtes 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Hôte 404} other {Hôtes 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Désactivé\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Noms de domaine\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} noms de domaine au maximum\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Commencez à écrire pour ajouter un domaine…\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Les Wildcards ne sont pas permises dans ce cas\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Les Wildcards ne sont pas prises en charge par cette autorité de certification.\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Forcer SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS activé\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"Sous-domaines HSTS\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Prise en charge de HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Utiliser le challenge DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Adresse eMail\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Aucun résultat trouvé\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Pourquoi n'en créez-vous pas un ?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Activé\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Une autorisation ou une règle d'accès est requise.\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Les noms d'utilisateurs autorisés doivent être uniques\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Adresse eMail ou mot de passe invalide\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Domaine invalide : {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Adresse eMail invalide\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"La longueur maximale est {max} caractère{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Trop de domaines, le maximum est {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Le maximum est {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"La longueur minimale est {min} caractère{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Le minimum est {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Les mots de passe doivent correspondre\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Ceci est obligatoire\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Expire : {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forkez-moi sur Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Bloquer les exploits courants\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Ressources du cache\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Préserver le chemin\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocoles\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Prise en charge de Websockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port de redirection\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hôtes\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP uniquement\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Chargement…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Connectez-vous à votre compte\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Configuration Nginx personnalisée\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Vous n'avez pas la permission de voir ce contenu.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Ramenez-moi à l'accueil\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Nous sommes désolés, mais la page que vous cherchez est introuvable\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… Vous avez découvert une page d'erreur\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Erreur\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} a été supprimé\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} a été désactivé\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} a été activé\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} a été renouvelé\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} a été enregistré\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Réussi\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Ajouter {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Supprimer {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Êtes-vous sûr de vouloir supprimer {object} ?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Modifier {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Il n'y a aucun {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} créé\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} supprimé\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} désactivé\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} activé\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} renouvelé\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} mis à jour\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Hors ligne\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"En ligne\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Options\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Mot de passe\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Générer un mot de passe aléatoire\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Masquer le mot de passe\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Afficher le mot de passe\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Masquer\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Gérer\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Voir uniquement\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Tous les éléments\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Éléments visibles\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Éléments créés uniquement\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Hôte proxy\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Nom d'hôte de redirection / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Hôtes proxy\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Publique\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Hôte de redirection\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Domaine de redirection\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Code HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Hôtes de redirection\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrateur\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Utilisateur standard\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Enregistrer\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Paramètre\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Paramètres\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Site par défaut\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Page 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Aucune réponse (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Page de félicitations\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML personnalisé\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Mettez votre contenu HTML personnalisé ici -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Redirection\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Commencez par créer votre compte administrateur.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Bienvenue !\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Se connecter\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Certificat SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Hôte destinataire\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Port d'entrée\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Mise à jour disponible : {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Utilisateur\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Modifier le mot de passe\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Confirmer le mot de passe\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Mot de passe actuel\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Modifier le profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Nom complet\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Se connecter en tant que {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Déconnexion\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nouveau mot de passe\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Pseudonyme\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Définir le mot de passe\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Définir les autorisations pour {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Passer au mode Sombre\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Passer au mode Lumineux\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nom d'utilisateur\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Utilisateurs\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/ga.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Liosta Rochtana\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Rial} other {Rialacha}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Úsáideoir} other {Úsáideoirí}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Nuair a bhíonn riail amháin ar a laghad ann, cuirfear an riail seo chun gach rud a dhiúltú leis an gceann deireanach.\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Tabhair faoi deara go gcuirfear na treoracha ceadaigh agus diúltaigh i bhfeidhm san ord a shainmhínítear iad.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Tabhair Údarú chuig an Sruth Uachtarach\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Inrochtana don Phobal\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Níl aon údarú bunúsach ag teastáil\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 nó 192.168.1.0/24 nó 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Sásaigh Aon\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Úsáideoir} other {Úsáideoirí}}, {rules} {rules, plural, one {Riail} other {Rialacha}} - Cruthaithe: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Liostaí Rochtana\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Cuir leis\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Cuir Suíomh leis\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Ceadaigh\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Dún\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Scrios\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Diúltaigh\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Díchumasaigh\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Íoslódáil\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Cuir in Eagar\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Cumasaigh\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Ceadanna\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Athnuachan\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Féach Sonraí\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Logaí Iniúchta\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Uath\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Cealaigh\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Teastas\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Teastas\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Eochair Teastais\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Teastas Idirmheánach\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Úsáid\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Níor sannadh aon deimhniú\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Ní úsáidfidh an t-óstach seo HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Dada\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Níor Úsáideadh\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Athnuachan an Teastais\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Teastais\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Teastas Saincheaptha\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Ní thacaítear le comhaid eochair atá cosanta le frása faire.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Ábhar Comhaid Dintiúir\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Éilíonn an breiseán seo comhad cumraíochta ina bhfuil comhartha API nó dintiúir eile do do sholáthraí.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Stórálfar an fhaisnéis seo mar théacs simplí sa bhunachar sonraí agus i gcomhad!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Soicindí Iolraithe\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Fág folamh chun luach réamhshocraithe na mbreiseán a úsáid. Líon na soicindí le fanacht le haghaidh iomadú DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Soláthraí DNS\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Roghnaigh Soláthraí...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Éilíonn an chuid seo roinnt eolais faoi Certbot agus a bhreiseáin DNS. Féach ar dhoiciméadacht na mbreiseán faoi seach, le do thoil.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Tá freastalaí aimsithe ag an bhfearann seo ach ní cosúil gur Bainisteoir Proxy Nginx atá ann. Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Theip ar sheiceáil an inrochtaineachta mar gheall ar earráid chumarsáide le site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Níl aon fhreastalaí ar fáil ag an bhfearann seo. Cinntigh le do thoil go bhfuil do fhearann ann agus go bhfuil sé ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith agus más gá, go bhfuil port 80 curtha ar aghaidh i do ródaire.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Tá rochtain ar do fhreastalaí agus ba cheart go mbeadh sé indéanta deimhnithe a chruthú.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Tá freastalaí aimsithe ag an bhfearann seo ach thug sé cód stádais gan choinne {code} ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Tá freastalaí aimsithe ag an bhfearann seo ach thug sé sonraí gan choinne ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Torthaí Tástála\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Ní mór na fearainn seo a bheith cumraithe cheana féin chun pointeáil chuig an suiteáil seo.\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"le Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Iarr Teastas nua\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Rochtain\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Údarú\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Údaruithe\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Suíomhanna Saincheaptha\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Ceann Scríbe\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Sonraí\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Ríomhphost\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Imeacht\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Éagaíonn\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Cód HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Port Isteach\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Ainm\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Prótacal\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Soláthraí\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Róil\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Rialacha\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Sásamh\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Gach\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Aon\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Scéim\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Foinse\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Stádas\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Cruthaithe: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Painéal Rialaithe\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Óstach 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Óstaigh\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Óstach 404} other {Óstaigh 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Míchumasaithe\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Ainmneacha Fearainn\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Uasmhéid d'ainmneacha fearainn: {count}\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Tosaigh ag clóscríobh chun fearann a chur leis...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Ní cheadaítear cártaí fiáine don chineál seo\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Ní thacaítear le cártaí fiáine don ÚD seo\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Fórsáil SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"Cumasaithe HSTS\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"Fo-fhearainn HSTS\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Tacaíocht HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Úsáid Dúshlán DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Seoladh ríomhphoist\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Níor aimsíodh aon torthaí\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Cén fáth nach gcruthaíonn tú ceann?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Cumasaithe\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Tá Údarú amháin nó Riail Rochtana amháin ag teastáil\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Ní mór d’ainmneacha úsáideora údaraithe a bheith uathúil\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Ríomhphost nó pasfhocal neamhbhailí\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Fearann neamhbhailí: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Seoladh ríomhphoist neamhbhailí\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Is é an fad uasta ná {max} carachtar{max, plural, one {} other {anna}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"An iomarca fearainn, is é {max} an t-uasmhéid\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Is é {max} an t-uasmhéid\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Is é an fad íosta ná {min} carachtar{min, plural, one {} other {anna}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Is é {min} an t-íosmhéid\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Ní mór pasfhocail a bheith mar a chéile\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Tá sé seo riachtanach\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Éagaíonn: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forc mé ar Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blocáil Easnaimh Choitianta\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Sócmhainní Taisce\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Cosán a Chaomhnú\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Prótacail\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Tacaíocht Websockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port Ar Aghaidh\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Scéim\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Óstaigh\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Amháin\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt trí DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt trí HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Ag lódáil…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Logáil isteach i do chuntas\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Cumraíocht Nginx Saincheaptha\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Cuir isteach do chumraíocht saincheaptha Nginx anseo ar do phriacal féin!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Níl rochtain agat chun seo a fheiceáil.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Tabhair abhaile mé\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Tá brón orainn ach níor aimsíodh an leathanach atá á lorg agat\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Úps… Fuair tú leathanach earráide díreach anois.\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Earráid\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"Scriosadh {object}\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"Tá {object} díchumasaithe\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"Tá {object} cumasaithe\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"Tá {object} athnuaite\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"Tá {object} sábháilte\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Rath\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Cuir {object} leis\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Scrios {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"An bhfuil tú cinnte gur mian leat an {object} seo a scriosadh?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Cuir in eagar {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Níl aon {objects} ann\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Cruthaithe {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Scriosadh {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Díchumasaithe {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Cumasaithe {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Athnuaite {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Nuashonraithe {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"As líne\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Ar líne\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Roghanna\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Pasfhocal\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Gin pasfhocal randamach\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Folaigh Pasfhocal\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Taispeáin Pasfhocal\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"I bhfolach\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Bainistigh\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Amharc Amháin\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Gach Míreanna\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Infheictheacht Míre\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Míreanna Cruthaithe Amháin\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Óstach Seachfhreastalaí\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Ainm Óstach / IP Ar Aghaidh\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Óstaigh Seachfhreastalaí\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Óstach Seachfhreastalaí} other {Óstaigh Seachfhreastalaí}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Poiblí\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Óstach Athsheolta\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Fearann Ar Aghaidh\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Cód HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Óstaigh Athsheolta\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Athsheoladh Óstach} other {Athsheoladh Óstaigh}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Rogha Ilghnéitheach\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Bogtha go buan\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Bogtha go sealadach\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Féach eile\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Atreorú sealadach\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Athsheoladh buan\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Riarthóir\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Úsáideoir Caighdeánach\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Sábháil\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Socrú\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Socruithe\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Suíomh Réamhshocraithe\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Leathanach 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Gan Freagra (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Leathanach Comhghairdeas\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Cad atá le taispeáint nuair a bhuaileann óstach anaithnid Nginx\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML saincheaptha\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Cuir isteach d’ábhar HTML saincheaptha anseo -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Atreorú\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Tosaigh trí do chuntas riarthóra a chruthú.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Fáilte!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Sínigh isteach\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Teastas SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Sruth\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Óstach Ar Aghaidh\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com nó 10.0.0.1 nó 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Port Isteach\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Sruthanna\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Sruth} other {Sruthanna}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Tástáil\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Nuashonrú ar Fáil: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Úsáideoir\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Athraigh Pasfhocal\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Deimhnigh Pasfhocal\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Pasfhocal Reatha\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Cuir Próifíl in Eagar\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Ainm Iomlán\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Sínigh isteach mar {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Logáil Amach\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Pasfhocal Nua\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Leasainm\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Socraigh Pasfhocal\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Socraigh Ceadanna do {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Athraigh go Mód Dorcha\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Athraigh go mód Solais\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Ainm úsáideora\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Úsáideoirí\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/hu.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Hátralévő tartalék kódok: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Mentse el ezeket a tartalék kódokat biztonságos helyre. Minden kód csak egyszer használható.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Kétfaktoros hitelesítés letiltása\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"2FA letiltása\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"A kétfaktoros hitelesítés letiltása kevésbé teszi biztonságossá a fiókját.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Letiltva\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"Elmentettem a tartalék kódjaimat\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Kétfaktoros hitelesítés engedélyezése\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Engedélyezve\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Adja meg az ellenőrző kódot\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Adja meg az ellenőrző kódot a letiltáshoz\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Újragenerálás\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Tartalék kódok újragenerálása\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Adjon meg egy ellenőrző kódot az új tartalék kódok generálásához. A régi kódok érvénytelenné válnak.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Titkos kulcs\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Olvassa be ezt a QR kódot a hitelesítő alkalmazásával, vagy adja meg a titkot manuálisan.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Állapot\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Kétfaktoros hitelesítés\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Ellenőrzés és engedélyezés\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Hozzáférési lista\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {szabály} other {szabály}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {felhasználó} other {felhasználó}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Ha legalább 1 szabály létezik, ez a mindent tiltó szabály utolsóként lesz hozzáadva\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Vegye figyelembe, hogy az engedélyező és tiltó direktívák a meghatározásuk sorrendjében lesznek alkalmazva.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Hitelesítés továbbítása az upstream felé\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Nyilvánosan elérhető\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Alapszintű hitelesítés nem szükséges\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 vagy 192.168.1.0/24 vagy 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Bármely teljesítése\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {felhasználó} other {felhasználó}}, {rules} {rules, plural, one {szabály} other {szabály}} - Létrehozva: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Hozzáférési listák\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Hozzáadás\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Útvonal hozzáadása\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Engedélyezés\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Bezárás\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Törlés\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Tiltás\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Letiltás\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Letöltés\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Szerkesztés\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Engedélyezés\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Engedélyek\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Megújítás\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Részletek megtekintése\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Audit naplók\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Automatikus\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Mégse\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Tanúsítvány\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Tanúsítvány\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Tanúsítvány kulcs\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Köztes tanúsítvány\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Használatban\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Nincs tanúsítvány hozzárendelve\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Ez a kiszolgáló nem fog HTTPS-t használni\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Nincs\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Nincs használatban\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Tanúsítvány megújítása\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Tanúsítványok\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Egyéni tanúsítvány\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Jelszóval védett kulcsfájlok nem támogatottak.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Hitelesítő fájl tartalma\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Ez a plugin egy konfigurációs fájlt igényel, amely API tokent vagy egyéb hitelesítő adatokat tartalmaz a szolgáltatóhoz\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Ezek az adatok sima szövegként lesznek tárolva az adatbázisban és egy fájlban!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagálási másodpercek\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Hagyja üresen a plugin alapértelmezett értékének használatához. Másodpercek száma a DNS propagálás megvárásához.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS szolgáltató\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Válasszon szolgáltatót...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Ez a szakasz némi ismeretet igényel a Certbot-ról és a DNS plugin-jeiről. Kérjük, olvassa el a megfelelő plugin dokumentációját.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Található szerver ezen a domain-en, de nem úgy tűnik, hogy Nginx Proxy Manager lenne. Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Nincs elérhető szerver ezen a domain-en. Kérjük, győződjön meg róla, hogy a domain létezik és arra az IP címre mutat, ahol az NPM példánya fut, és szükség esetén a 80-as port továbbítva van a routerében.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"A szerver elérhető és a tanúsítványok létrehozása lehetséges lesz.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Található szerver ezen a domain-en, de váratlan {code} státuszkódot adott vissza. Ez az NPM szerver? Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Található szerver ezen a domain-en, de váratlan adatot adott vissza. Ez az NPM szerver? Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Teszt eredmények\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Ezeknek a domain-eknek már konfigurálva kell lenniük, hogy erre a telepítésre mutassanak.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Kulcs típus\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"Az RSA széles körben kompatibilis, az ECDSA gyorsabb és biztonságosabb, de nem biztos, hogy régebbi rendszerek támogatják\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"Let's Encrypt-tel\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Új tanúsítvány kérelmezése\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Hozzáférés\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Jogosultság\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Jogosultságok\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Egyéni útvonalak\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Cél\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Részletek\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"E-mail\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Esemény\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Lejár\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP kód\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Bejövő port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Név\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokoll\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Szolgáltató\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Szerepkörök\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Szabályok\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Teljesítés\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Összes\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Bármely\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Séma\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Forrás\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Állapot\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Létrehozva: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Vezérlőpult\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404-es Kiszolgáló\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404-es Kiszolgálók\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404-es Kiszolgáló} other {404-es Kiszolgálók}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Letiltva\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domain nevek\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maximum {count} domain név\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Kezdjen el gépelni domain hozzáadásához...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Helyettesítő karakterek nem engedélyezettek ennél a típusnál\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Helyettesítő karakterek nem támogatottak ennél a CA-nál\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"SSL kényszerítése\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS engedélyezve\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS aldomain-ek\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 támogatás\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"DNS Challenge használata\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"E-mail cím\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nincs találat\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Miért nem hoz létre egyet?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Engedélyezve\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Legalább egy jogosultság vagy egy hozzáférési szabály szükséges\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"A jogosultsági felhasználóneveknek egyedieknek kell lenniük\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Érvénytelen e-mail vagy jelszó\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Érvénytelen domain: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Érvénytelen e-mail cím\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximális hossz {max} karakter\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Túl sok domain, a maximum {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"A maximum {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimális hossz {min} karakter\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"A minimum {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"A jelszavaknak egyezniük kell\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Ez kötelező\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Lejár: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork-olj a GitHub-on\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Gyakori exploitok blokkolása\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Erőforrások gyorsítótárazása\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Útvonal megőrzése\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokollok\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets támogatás\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Továbbító port\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Séma\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Kiszolgálók\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Csak HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt DNS-en keresztül\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt HTTP-n keresztül\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Betöltés…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Ellenőrző kód\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Adja meg a kódot\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Adja meg a kódot a hitelesítő alkalmazásából\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"Kétfaktoros hitelesítés\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Ellenőrzés\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Jelentkezzen be a fiókjába\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Egyéni Nginx konfiguráció\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Adja meg az egyéni Nginx konfigurációját itt, saját felelősségére!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Nincs jogosultsága ennek megtekintéséhez.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Vigyen haza\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Sajnáljuk, de a keresett oldal nem található\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Hoppá… Hibás oldalra talált\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Hiba\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} törölve lett\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} letiltva lett\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} engedélyezve lett\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} megújítva lett\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} mentve lett\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Sikeres\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"{object} hozzáadása\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"{object} törlése\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Biztosan törölni szeretné ezt: {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"{object} szerkesztése\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Nincsenek {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} létrehozva\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} törölve\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} letiltva\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} engedélyezve\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} megújítva\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} frissítve\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Beállítások\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Jelszó\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Véletlenszerű jelszó generálása\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Jelszó elrejtése\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Jelszó megjelenítése\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Rejtett\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Kezelés\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Csak megtekintés\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Összes elem\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Elemek láthatósága\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Csak létrehozott elemek\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Kiszolgáló\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Továbbító hostnév / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Kiszolgálók\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Kiszolgáló} other {Proxy Kiszolgálók}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Nyilvános\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Átirányító Kiszolgáló\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Továbbító domain\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP kód\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Átirányító Kiszolgálók\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Átirányító Kiszolgáló} other {Átirányító Kiszolgálók}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Többszörös választás\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Véglegesen áthelyezve\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Ideiglenesen áthelyezve\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Lásd másik\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Ideiglenes átirányítás\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Végleges átirányítás\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Adminisztrátor\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Általános felhasználó\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Mentés\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Beállítás\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Beállítások\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Alapértelmezett oldal\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404-es oldal\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Nincs válasz (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Gratulálunk oldal\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Mit mutasson az Nginx ismeretlen Kiszolgáló esetén\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Egyéni HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Adja meg az egyéni HTML tartalmát itt -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Átirányítás\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Kezdje az admin fiók létrehozásával.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Üdvözöljük!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Bejelentkezés\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL tanúsítvány\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Továbbító kiszolgáló\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com vagy 10.0.0.1 vagy 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Bejövő port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streamek\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Stream}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Teszt\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Frissítés elérhető: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Felhasználó\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Jelszó megváltoztatása\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Jelszó megerősítése\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Jelenlegi jelszó\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Profil szerkesztése\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Teljes név\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Bejelentkezés mint {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Kijelentkezés\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Új jelszó\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Becenév\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Jelszó beállítása\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Engedélyek beállítása {name} számára\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Váltás sötét módra\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Váltás világos módra\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"Kétfaktoros hitelesítés\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Felhasználónév\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Felhasználók\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/id.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Daftar Akses\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Aturan} other {Aturan}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Pengguna} other {Pengguna}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Jika setidaknya 1 aturan ada, aturan tolak semua ini akan ditambahkan paling akhir\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Perhatikan bahwa direktif izinkan dan tolak akan diterapkan sesuai urutan yang didefinisikan.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Teruskan Auth ke Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Dapat Diakses Publik\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Tidak perlu basic auth\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 atau 192.168.1.0/24 atau 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Penuhi Salah Satu\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Pengguna} other {Pengguna}}, {rules} {rules, plural, one {Aturan} other {Aturan}} - Dibuat: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Daftar Akses\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Tambah\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Tambah Lokasi\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Izinkan\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Tutup\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Hapus\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Tolak\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Nonaktifkan\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Unduh\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Edit\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aktifkan\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Izin\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Perpanjang\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Lihat Detail\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Log Audit\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Otomatis\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Batal\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Sertifikat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Sertifikat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Kunci Sertifikat\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Sertifikat Intermediate\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Digunakan\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Tidak ada sertifikat yang ditetapkan\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Host ini tidak akan menggunakan HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Tidak Ada\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Tidak Digunakan\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Perpanjang Sertifikat\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Sertifikat\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Sertifikat Kustom\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Berkas kunci yang dilindungi frasa sandi tidak didukung.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Konten File Kredensial\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Plugin ini memerlukan file konfigurasi yang berisi token API atau kredensial lain untuk penyedia Anda\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Data ini akan disimpan sebagai teks biasa di database dan dalam file!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Detik Propagasi\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Biarkan kosong untuk menggunakan nilai baku plugin. Jumlah detik menunggu propagasi DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Penyedia DNS\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Pilih Penyedia...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Ada server yang ditemukan pada domain ini tetapi tampaknya bukan Nginx Proxy Manager. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Tidak ada server yang tersedia pada domain ini. Pastikan domain Anda ada dan mengarah ke IP tempat instance NPM berjalan dan bila perlu port 80 diteruskan di router Anda.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Server Anda dapat dijangkau dan pembuatan sertifikat seharusnya memungkinkan.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Ada server yang ditemukan pada domain ini tetapi mengembalikan kode status tak terduga {code}. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Ada server yang ditemukan pada domain ini tetapi mengembalikan data yang tidak terduga. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Hasil Uji\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Domain ini harus sudah dikonfigurasi agar mengarah ke instalasi ini.\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"dengan Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Minta Sertifikat Baru\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Akses\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Otorisasi\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Otorisasi\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Lokasi Kustom\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Tujuan\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Detail\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Peristiwa\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Kedaluwarsa\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Kode HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Port Masuk\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nama\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Penyedia\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Peran\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Aturan\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Pemenuhan\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Semua\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Salah Satu\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Skema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Sumber\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Dibuat: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dasbor\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Host 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Host 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host 404} other {Host 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Nonaktif\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Nama Domain\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maksimum {count} nama domain\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Mulai mengetik untuk menambahkan domain...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcard tidak diizinkan untuk tipe ini\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcard tidak didukung untuk CA ini\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Paksa SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Diaktifkan\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Subdomain\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Dukungan HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Gunakan DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Alamat email\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Tidak ada hasil\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Mengapa tidak membuatnya?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Aktif\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Setidaknya satu Otorisasi atau satu Aturan Akses diperlukan\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Nama pengguna otorisasi harus unik\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Email atau kata sandi tidak valid\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Domain tidak valid: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Alamat email tidak valid\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Panjang maksimum adalah {max} karakter{max, plural, one {} other {}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Terlalu banyak domain, maksimum {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maksimum adalah {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Panjang minimum adalah {min} karakter{min, plural, one {} other {}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum adalah {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Kata sandi harus cocok\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Ini wajib diisi\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Kedaluwarsa: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork saya di GitHub\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokir Eksploit Umum\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache Aset\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Pertahankan Path\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokol\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Dukungan Websocket\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port Terusan\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Skema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Host\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Saja\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Memuat…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Masuk ke akun Anda\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Konfigurasi Nginx Kustom\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Masukkan konfigurasi Nginx kustom Anda di sini dengan risiko Anda sendiri!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Anda tidak memiliki akses untuk melihat ini.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Bawa saya pulang\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Maaf, halaman yang Anda cari tidak ditemukan\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Ups… Anda baru saja menemukan halaman error\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Kesalahan\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} telah dihapus\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} telah dinonaktifkan\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} telah diaktifkan\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} telah diperpanjang\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} telah disimpan\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Berhasil\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Tambah {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Hapus {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Apakah Anda yakin ingin menghapus {object} ini?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Edit {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Tidak ada {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} dibuat\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} dihapus\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} dinonaktifkan\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} diaktifkan\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} diperpanjang\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} diperbarui\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opsi\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Kata sandi\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Buat kata sandi acak\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Sembunyikan Kata Sandi\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Tampilkan Kata Sandi\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Tersembunyi\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Kelola\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Hanya Lihat\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Semua Item\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Visibilitas Item\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Hanya Item yang Dibuat\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Host Proxy\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Hostname / IP Terusan\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Host Proxy\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host Proxy} other {Host Proxy}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Publik\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Host Pengalihan\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Domain Terusan\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Kode HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Host Pengalihan\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host Pengalihan} other {Host Pengalihan}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Banyak Pilihan\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Pindah permanen\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Pindah sementara\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Lihat lainnya\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Pengalihan sementara\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Pengalihan permanen\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Pengguna Standar\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Simpan\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Pengaturan\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Pengaturan\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Situs Default\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Halaman 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Tidak Ada Respons (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Halaman Ucapan Selamat\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Apa yang ditampilkan saat Nginx diakses dengan Host yang tidak dikenal\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML Kustom\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Masukkan konten HTML kustom Anda di sini -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Alihkan\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Mulai dengan membuat akun admin Anda.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Selamat datang!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Masuk\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Sertifikat SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Host Terusan\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com atau 10.0.0.1 atau 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Port Masuk\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Stream}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Uji\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Pembaruan Tersedia: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Pengguna\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Ubah Kata Sandi\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Konfirmasi Kata Sandi\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Kata Sandi Saat Ini\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Edit Profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Nama Lengkap\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Masuk sebagai {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Keluar\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Kata Sandi Baru\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Nama Panggilan\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Atur Kata Sandi\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Atur Izin untuk {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Beralih ke mode gelap\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Beralih ke mode terang\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nama pengguna\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Pengguna\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/it.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Lista di Accesso\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Regola} other {Regole}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Utente} other {Utenti}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Quando esiste almeno 1 regola, questa regola di negazione verrà aggiunta per ultima\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Nota che le direttive di allow e deny saranno applicate nell'ordine in cui sono definite.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Passa Autenticazione all'Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Accessibile Pubblicamente\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Nessuna autenticazione base richiesta\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Soddisfa Qualsiasi\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Utente} other {Utenti}}, {rules} {rules, plural, one {Regola} other {Regole}} - Creato: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Liste di Accesso\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Aggiungi\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Aggiungi Percorso\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Chiudi\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Elimina\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Disabilita\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Scarica\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Modifica\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Abilita\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permessi\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Rinnova\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Visualizza Dettagli\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Log di Audit\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Annulla\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificato\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificato\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Chiave del Certificato\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Certificato Intermedio\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Uso\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Nessun certificato assegnato\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Questo host non utilizzerà HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Nessuno\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Non in Uso\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Rinnova Certificato\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificati\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Certificato Personalizzato\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"I file di chiave protetti da passphrase non sono supportati.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Contenuto File Credenziali\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Questo plugin richiede un file di configurazione contenente un token API o altre credenziali per il tuo provider\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Questi dati saranno memorizzati in chiaro nel database e in un file!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Secondi di Propagazione\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Lascia vuoto per usare il valore predefinito del plugin. Numero di secondi da attendere per la propagazione DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Provider DNS\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"È stato trovato un server su questo dominio, ma non sembra essere Nginx Proxy Manager. Assicurati che il dominio punti all'IP dove è in esecuzione NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Nessun server disponibile su questo dominio. Assicurati che il dominio esista e punti all'IP corretto e che la porta 80 sia inoltrata.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Il server è raggiungibile e la creazione dei certificati è possibile.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"È stato trovato un server su questo dominio ma ha restituito un codice di stato imprevisto {code}. È il server NPM? Controlla che il dominio punti correttamente all'IP.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"È stato trovato un server su questo dominio ma ha restituito dati imprevisti. È il server NPM? Controlla che il dominio punti correttamente all'IP.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Risultati Test\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Questi domini devono già essere configurati per puntare a questa installazione.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Tipo di Chiave\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA è ampiamente compatibile, ECDSA è più veloce e sicuro ma potrebbe non essere supportato da sistemi più vecchi\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"con Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Richiedi un nuovo Certificato\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Accesso\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorizzazione\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorizzazioni\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Percorsi Personalizzati\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destinazione\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Dettagli\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Evento\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Scadenza\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Codice HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Porta in Ingresso\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nome\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocollo\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Provider\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Ruoli\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Regole\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Condizione\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Tutte\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Qualsiasi\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Origine\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Stato\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Creato: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Host 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Hosts 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host 404} other {Hosts 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Disabilitato\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Nomi di Dominio\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Massimo {count} nomi di dominio\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Inizia a digitare per aggiungere un dominio...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcard non consentite per questo tipo\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcard non supportate per questa CA\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Forza SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Abilitato\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"Sottodomini HSTS\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Supporto HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Usa Challenge DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Indirizzo Email\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nessun risultato trovato\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Perché non ne crei uno?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Abilitato\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"È richiesta almeno un'Autorizzazione o una Regola di Accesso\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"I nomi utente devono essere unici\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Email o password non validi\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Dominio non valido: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Indirizzo email non valido\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Lunghezza massima {max} caratter{max, plural, one {e} other {i}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Troppi domini, massimo {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Massimo {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Lunghezza minima {min} caratter{min, plural, one {e} other {i}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimo {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Le password devono coincidere\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Campo obbligatorio\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Scade: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forkami su GitHub\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blocca Exploit Comuni\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache degli Asset\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Preserva Percorso\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocolli\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Supporto WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Porta di Destinazione\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Host\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Solo HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Caricamento…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Accedi al tuo account\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Configurazione Nginx Personalizzata\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Inserisci qui la configurazione Nginx personalizzata a tuo rischio!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Non hai accesso per visualizzare questa pagina.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Torna alla Home\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Spiacenti, la pagina richiesta non è stata trovata\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… Hai trovato una pagina di errore\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Errore\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} è stato eliminato\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} è stato disabilitato\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} è stato abilitato\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} è stato rinnovato\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} è stato salvato\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Successo\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Aggiungi {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Elimina {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Sei sicuro di voler eliminare questo {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Modifica {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Non ci sono {objects} presenti\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} creato\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} eliminato\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} disabilitato\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} abilitato\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} rinnovato\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} aggiornato\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opzioni\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Password\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Genera password casuale\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Nascondi Password\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Mostra Password\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Nascosto\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Gestisci\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Sola Lettura\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Tutti gli Elementi\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Visibilità Elementi\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Solo Elementi Creati\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Hostname / IP di Destinazione\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host Proxy} other {Host Proxy}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Pubblico\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Host di Reindirizzamento\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Dominio di Destinazione\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Codice HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Host di Reindirizzamento\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host di Reindirizzamento} other {Host di Reindirizzamento}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Amministratore\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Utente Standard\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Salva\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Impostazione\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Impostazioni\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Sito Predefinito\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Pagina 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Nessuna Risposta (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Pagina di Congratulazioni\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Cosa mostrare quando Nginx riceve una richiesta da un host sconosciuto\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML Personalizzato\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Inserisci qui il tuo contenuto HTML personalizzato -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Reindirizza\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Inizia creando il tuo account amministratore.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Benvenuto!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Accedi\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Certificato SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Host di Destinazione\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Porta in Ingresso\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Stream}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Aggiornamento Disponibile: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Utente\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Cambia Password\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Conferma Password\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Password Attuale\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Modifica Profilo\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Nome Completo\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Accedi come {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Disconnetti\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nuova Password\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Soprannome\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Imposta Password\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Imposta Permessi per {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Passa alla modalità Scura\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Passa alla modalità Chiara\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nome Utente\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Utenti\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/ja.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"アクセスリスト\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} ルール\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} ユーザー\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"少なくとも 1 つのルールが存在する場合、 他のすべてを拒否するルールが最後に追加されます\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"許可コマンドと拒否コマンドは定義された順番で適用されます\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"認証情報をアップストリームに送信する\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"公開されたアクセス\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"ベーシック認証を使用しません\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"いずれかを満たす\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} ユーザー, {rules} ルール - 作成日時: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"アクセスリスト\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"追加\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"場所を追加\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"閉じる\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"削除\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"無効化\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"ダウンロード\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"編集\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"有効化\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"権限\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"更新\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"詳細\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"監査ログ\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"キャンセル\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"証明書\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"証明書\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"証明書キー\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"中間証明書\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"使用中\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"証明書が割り当てられていません\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"このホストはHTTPSを使用しません\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"無し\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"未使用\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"証明書を更新\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"証明書\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"カスタム証明書\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"パスワードによって保護されたキーファイルはサポートされていません\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"資格情報ファイルの内容\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"このプラグインはプロバイダーのAPIキーか認証情報を含む設定ファイルが必要です\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"このデータはファイルとデータベースにプレーンテキストとして保存されます\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"DNS伝播時間(秒)\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"DNSの伝搬時間を秒で指定します。空にするとデフォルトの値を使用します。\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNSプロバイダー\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"このドメインはNginx Proxy Managerではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"このドメインには利用可能なサーバーがありません。ドメインが存在し、NPMインスタンスのIPアドレスを指していること、必要に応じてルーターでポート80が転送されていることを確認してください。\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"サーバーへ到達可能であり、証明書の作成が可能です。\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"このドメインでサーバーが見つかりましたが予期しないステータスコード {code} を返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"このドメインでサーバーが見つかりましたが予期しないデータを返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"テスト結果\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"これらのドメインは、すでにこのインストール先を指すように設定されている必要がありますあ.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"鍵タイプ\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSAは広く互換性があり、ECDSAはより高速で安全ですが、古いシステムではサポートされていない場合があります\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"Let's Encryptを使用する\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"新しい証明書を作成\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"アクセス\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"認証\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"認証\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"カスタムロケーション\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"宛先\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"詳細\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"イベント\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"期限切れ\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"アクセス\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"受信ポート\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"名前\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"プロトコル\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"プロバイダー\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roles\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"ルール\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfy\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"すべて\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"いずれか\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"スキーム\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"ソース\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"ステータス\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"作成日時: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"ダッシュボード\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 ホスト\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 ホスト\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} 404 ホスト\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"無効化\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"ドメイン名\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count}のドメイン名が最大です\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"追加するドメインを入力...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"ワイルドカードはこのタイプでは許可されていません\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"ワイルドカードはこのCAではサポートされていません\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"SSLを強制\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTSを有効化\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTSサブドメイン\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2サポート\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"DNSチャレンジを使用\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Emailアドレス\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"見つかりませんでした\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"作ってみましょう\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"有効\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"少なくとも一つの認証またはアクセスルールが必要です\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"認証のユーザー名は他と同じ名前は使用できません\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"無効なemailまたはパスワード\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"無効なドメイン: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"無効なemailアドレス\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"文字数は長くとも{max}文字です\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"ドメインが多すぎます, 最大値は{max}です\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"最大値は{max}です\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"文字数は少なくとも{min}文字です\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"最小値は{min}です\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"パスワードは一致する必要があります\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"必須項目です\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"有効期限: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork me on Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"一般的なエクスプロイトをブロックする\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"アセットをキャッシュする\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"パスワードは一致する必要があります\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"プロトコル\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websocketsサポート\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"転送ポート\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"スキーム\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"ホスト\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Only\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Loading…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"アカウントにログイン\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"カスタムNginx設定\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Enter your custom Nginx configuration here at your own risk!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"これを表示する権限がありません\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"ホームに戻る\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"申し訳ありませんが探しているページは見つかりませんでした\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"おっと... エラーページにたどり着いてしまったようです\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"エラー\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object}は削除されました\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object}は無効化されました\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object}は有効化されました\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object}は再作成されました\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object}は保存されました\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"成功\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"{object}を追加\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"{object}を削除\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"本当に{object}を削除しますか?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"{object}を編集\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"{objects}はありません\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object}を作成済み\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object}を削除済み\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object}を無効化済み\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object}を有効化済み\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object}を再作成済み\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object}を更新済み\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Options\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"パスワード\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"ランダムなパスワードを生成\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"パスワードを隠す\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"パスワードを表示する\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"非公開\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"管理\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"表示のみ\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"すべて\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"可視性\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"作成したもののみ\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"プロキシホスト\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"転送ホスト名/IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"プロキシホスト\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} プロキシホスト\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Public\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"リダイレクトホスト\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"転送ホスト\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"リダイレクトホスト\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} リダイレクトホスト\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"管理者\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"一般ユーザー\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"保存\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"設定\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"設定\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"デフォルトサイト\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404ページ\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"返答しない (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"設定ページ\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"不明なホストを要求されたときにNginxが何を返すかを設定します\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"カスタムHTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Enter your custom HTML content here -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"リダイレクト\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"管理者アカウントを作成して始めましょう\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"ようこそ!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"サインイン\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL証明書\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"ストリーム\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"転送ホスト\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"受信ポート\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"ストリーム\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} ストリーム\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"テスト\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"ユーザー\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"変更するパスワード\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"変更するパスワードを確認\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"現在のパスワード\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"プロフィールを編集\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"フルネーム\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"{name}としてサインイン\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"ログアウト\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"新しいパスワード\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"ニックネーム\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"パスワードを設定\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"{name}に権限を設定\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"ダークモードに変更\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"ライトモードに変更\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"ユーザー名\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"ユーザー\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/ko.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"접근 정책\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count}개의 정책\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count}명의 사용자\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"규칙이 하나라도 있으면 아래 ‘전체 거부’ 규칙이 마지막에 추가됩니다.\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"허용/거부 규칙은 정의된 순서대로 적용됩니다.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"인증 정보를 원본 서버로 전달\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"누구나 접근 가능\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"기본 인증 필요 없음\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 / 192.168.1.0/24 / IPv6\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"조건 중 하나라도 충족\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users}명 {users, plural, one {사용자} other {사용자}}, {rules}개 {rules, plural, one {규칙} other {규칙}} - 생성일: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"접근 정책\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"추가\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"경로 추가\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"허용\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"닫기\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"삭제\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"거부\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"비활성화\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"다운로드\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"편집\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"활성화\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"권한\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"갱신\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"자세히 보기\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"감사 로그\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"자동\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"취소\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"인증서\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"인증서\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"인증서 키\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"중간 인증서\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"사용 중\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"지정된 인증서 없음\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"이 호스트는 HTTPS를 사용하지 않습니다.\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"없음\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"사용 안 함\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"인증서 갱신\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"인증서\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"사용자 지정 인증서\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"비밀번호로 보호된 키 파일은 지원되지 않습니다.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"DNS 자격 증명 입력\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"이 플러그인은 API 토큰 등이 포함된 설정 파일이 필요합니다.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"입력한 정보는 데이터베이스와 파일에 평문으로 저장됩니다.\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"DNS 전파 시간\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"비워두면 기본값을 사용합니다. DNS 전파를 기다리는 시간(초)입니다.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS 공급자\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"공급자를 선택하세요...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"해당 도메인에 접근 가능한 서버가 없습니다. 도메인이 존재하며 NPM이 실행되는 IP를 가리키고, 필요하면 라우터에서 80포트가 포워딩되어 있는지 확인하세요.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"서버에 정상적으로 접근할 수 있으며 인증서 발급이 가능합니다.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"해당 도메인에서 서버가 발견되었지만 예상치 못한 상태 코드 {code}를 반환했습니다. NPM 서버가 맞는지 확인하세요.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"서버가 응답했지만 예상치 못한 데이터를 반환했습니다. NPM 서버가 맞는지 확인하세요.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"테스트 결과\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"도메인이 이 서버를 가리키도록 설정되어 있어야 합니다.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"키 유형\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA는 호환성이 넓고, ECDSA는 더 빠르고 안전하지만 오래된 시스템에서 지원되지 않을 수 있습니다\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"Let's Encrypt 사용\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"새 인증서 요청\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"접근 정책\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"인증 사용자\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"인증 사용자\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"사용자 지정 경로\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"전달 대상\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"기본 설정\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"이메일\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"이벤트\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"만료일\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP 코드\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"수신 포트\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"이름\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"프로토콜\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"공급자\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"권한\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"IP 정책\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"조건 방식\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"모두 충족\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"하나라도 충족\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"프로토콜\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"도메인\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"상태\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"생성일: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"대시보드\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 호스트\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 호스트\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count}개의 404 호스트\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"비활성화\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"도메인 이름\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"최대 {count}개의 도메인 이름\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"도메인을 입력해주세요.\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"HTTP 방식으로는 와일드카드 인증서를 발급할 수 없습니다.\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"이 인증 기관(CA)은 와일드카드를 지원하지 않습니다.\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"SSL 강제 적용\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS 활성화\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS 서브도메인 포함\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 지원\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"DNS 챌린지 사용\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"이메일 주소\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"검색 결과 없음\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"하나 만들어 보는 건 어떨까요?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"활성화\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"인증 또는 접근 규칙 중 하나는 반드시 필요합니다.\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"인증 사용자 이름은 중복될 수 없습니다.\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"이메일 또는 비밀번호가 잘못되었습니다.\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"잘못된 도메인: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"잘못된 이메일 주소입니다.\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"최대 길이는 {max}자입니다.\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"도메인이 너무 많습니다. 최대 {max}개까지 가능합니다.\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"최댓값은 {max}입니다.\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"최소 길이는 {min}자입니다.\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"최솟값은 {min}입니다.\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"비밀번호가 일치해야 합니다.\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"필수 항목입니다.\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"만료일: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"GitHub에서 포크하기\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"일반적인 공격 차단\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"정적 에셋 캐싱\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"요청 경로 유지\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"프로토콜\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"웹소켓 지원\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"전달할 포트\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"프로토콜\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"호스트 목록\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP 전용\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt (DNS 방식)\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt (HTTP 방식)\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"불러오는 중…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"로그인\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"사용자 지정 Nginx 설정\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# 위험을 감수하고 여기에 사용자 지정 Nginx 설정을 입력하세요!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"이 내용을 볼 권한이 없습니다.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"홈으로 이동\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"죄송합니다. 찾으시는 페이지를 찾을 수 없습니다.\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"이런… 오류 페이지에 도착했습니다.\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"오류\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object}이(가) 삭제되었습니다.\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object}이(가) 비활성화되었습니다.\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object}이(가) 활성화되었습니다.\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object}이(가) 갱신되었습니다.\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object}이(가) 저장되었습니다.\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"성공\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"{object} 추가\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"{object} 삭제\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"이 {object}을(를) 정말 삭제하시겠습니까?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"{object} 편집\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"{objects}이(가) 없습니다.\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object}이(가) 생성됨\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object}이(가) 삭제됨\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object}이(가) 비활성화됨\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object}이(가) 활성화됨\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object}이(가) 갱신됨\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object}이(가) 업데이트됨\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"비활성화\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"활성화\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"옵션\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"비밀번호\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"무작위 비밀번호 생성\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"비밀번호 숨기기\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"비밀번호 표시\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"숨김\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"관리\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"보기 전용\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"모든 항목\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"항목 표시 설정\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"내가 만든 항목만\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"프록시 호스트\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"전달할 호스트명 / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"프록시 호스트\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count}개의 프록시 호스트\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"공개\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"리다이렉션 호스트\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"전달할 도메인\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP 코드\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"리다이렉션 호스트\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count}개의 리다이렉션 호스트\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiple Choices\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Moved permanently\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Moved temporarily\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 See other\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Temporary redirect\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Permanent redirect\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"관리자\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"일반 사용자\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"저장\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"설정\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"설정\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"기본 사이트\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 페이지\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"응답 없음 (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"축하 페이지\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"알 수 없는 호스트로 요청이 들어왔을 때 표시할 내용\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"사용자 지정 HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- 여기에 사용자 정의 HTML 내용을 입력하세요. -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"리다이렉트\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"관리자 계정을 만들어 시작하세요.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"환영합니다!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"로그인\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL 인증서\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"호스트 스트림\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"전달할 호스트\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com / 10.0.0.1 / IPv6\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"수신 포트\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"호스트 스트림\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count}개의 호스트 스트림\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"테스트\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"업데이트 가능: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"사용자\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"비밀번호 변경\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"비밀번호 확인\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"현재 비밀번호\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"프로필 편집\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"전체 이름\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"{name}으로 로그인\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"로그아웃\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"새 비밀번호\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"닉네임\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"비밀번호 설정\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"{name}의 권한 설정\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"다크 모드로 전환\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"라이트 모드로 전환\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"사용자 이름\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"사용자\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/lang-list.json",
    "content": "{\n  \"locale-en-US\": {\n    \"defaultMessage\": \"English\"\n  },\n  \"locale-es-ES\": {\n    \"defaultMessage\": \"Español\"\n  },\n  \"locale-et-EE\": {\n    \"defaultMessage\": \"Eesti\"\n  },\n  \"locale-ie-GA\": {\n    \"defaultMessage\": \"Gaeilge\"\n  },\n  \"locale-de-DE\": {\n    \"defaultMessage\": \"German\"\n  },\n  \"locale-pt-PT\": {\n\t\"defaultMessage\": \"Português (Europeu)\"\n  },\n  \"locale-fr-FR\": {\n    \"defaultMessage\": \"Français\"\n  },\n  \"locale-id-ID\": {\n    \"defaultMessage\": \"Bahasa Indonesia\"\n  },\n  \"locale-ja-JP\": {\n    \"defaultMessage\": \"日本語\"\n  },\n  \"locale-ru-RU\": {\n    \"defaultMessage\": \"Русский\"\n  },\n  \"locale-sk-SK\": {\n    \"defaultMessage\": \"Slovenčina\"\n  },\n  \"locale-cs-CZ\": {\n    \"defaultMessage\": \"Čeština\"\n  },\n  \"locale-zh-CN\": {\n    \"defaultMessage\": \"中文\"\n  },\n  \"locale-pl-PL\": {\n    \"defaultMessage\": \"Polski\"\n  },\n  \"locale-it-IT\": {\n    \"defaultMessage\": \"Italiano\"\n  },\n  \"locale-vi-VN\": {\n    \"defaultMessage\": \"Tiếng Việt\"\n  },\n  \"locale-nl-NL\": {\n    \"defaultMessage\": \"Nederlands\"\n  },\n  \"locale-ko-KR\": {\n    \"defaultMessage\": \"한국어\"\n  },\n  \"locale-bg-BG\": {\n    \"defaultMessage\": \"Български\"\n  },\n  \"locale-tr-TR\": {\n    \"defaultMessage\": \"Türkçe\"\n  },\n  \"locale-hu-HU\": {\n    \"defaultMessage\": \"Magyar\"\n  },\n  \"locale-no-NO\": {\n    \"defaultMessage\": \"Norsk\"\n  }\n}\n"
  },
  {
    "path": "frontend/src/locale/src/nl.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Toegangslijst\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Regel} other {Regels}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Gebruiker} other {Gebruikers}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Als er minimaal 1 regel bestaat, wordt deze regel als laatste toegevoegd\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Onthoud dat de regels van boven naar beneden worden toegevoegd.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Pass Auth to Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Publiekelijk toegankelijk\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Geen basisautentificatie vereist\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Voldoe aan elke\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Gebruiker} other {Gebruikers}}, {rules} {rules, plural, one {Regel} other {Regels}} - Aangemaakt: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Toegangslijsten\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Toevoegen\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Locatie Toevoegen\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Sluiten\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Verwijderen\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Uitzetten\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Download\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Bewerken\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aanzetten\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Rechten\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Vernieuwen\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Bekijk Details\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Logboeken\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Annuleren\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificaat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificaat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Certificaat Sleutel\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Intermediate Certificaat\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"In Gebruik\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Geen certificaat toegewezen\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Deze host gebruikt geen HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Geen\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Niet Gebruikt\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Certificaat Vernieuwen\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificaten\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Aangepast Certificaat\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Sleutels met een wachtzin zijn niet ondersteund.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Credentials File Content\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Deze plugin vereist een configuratiebestand met een API token of andere gegevens van de provider.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Deze data zal worden opgeslagen als plaintext in de database en in een bestand!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Verwerkingstijd (seconden)\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Laat leeg om de standaardwaarde van de plugin te gebruiken. Aantal seconden om te wachten op DNS propagatie.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS Provider\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Er is een server gevonden op deze domeinnaam, maar dat lijkt niet Nginx Proxy Manager te zijn. Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Er is geen server beschikbaar op dit domein. Zorg ervoor dat je domein bestaat en naar het IP waar je NPM instance draait wijst en eventueel port 80 wordt doorgegeven in je router.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Jouw server is bereikbaar en certificaten kunnen worden aangemaakt.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Er is een server gevonden op deze domeinnaam, maar heeft een onverwachte statuscode ({code}) teruggegeven. Is dat de NPM server? Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Er is een server gevonden op deze domeinnaam, maar heeft een onverwachte gegevens teruggegeven. Is dat de NPM server? Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Testresultaten\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Deze domeinen moeten al worden geconfigureerd om naar deze installatie te wijzen.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Sleuteltype\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA is breed compatibel, ECDSA is sneller en veiliger maar wordt mogelijk niet ondersteund door oudere systemen\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"met Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Vraag een nieuwe Certificaat aan\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Toegang\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Authorizatie\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Authorizaties\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Aangepaste Locaties\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Doel\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Details\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Gebeurtenis\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Verloopt\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Inkomende Poort\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Naam\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Provider\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Rollen\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Regels\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Vervul\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Alle\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Elke\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Bron\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Aangemaakt: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Host\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hosts\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Host} other {404 Hosts}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Uitgezet\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domeinnamen\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} domeinnamen toegestaan\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Voeg een domeinnaam toe...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards zijn niet toegestaan voor dit type\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards zijn niet ondersteund voor deze CA\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Forceer SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Aangezet\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Subdomein\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Ondersteuning\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Gebruik DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"E-mailadres\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Geen resultaten gevonden\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Waarom niet een maken?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Aangezet\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Minimaal één authorizatie- of één toegangsregel is vereist\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Gebruikersnamen moeten uniek zijn\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Ongeldige email of wachtwoord\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Ongeldige domeinnaam: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Ongeldig e-mailadres\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximale lengte is {max} karakter{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Te veel domeinnamen, max is {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximale is {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimale lengte is {min} karakter{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimale is {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Wachtwoorden moeten overeenkomen\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Dit is verplicht\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Verloopt: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Maak een Fork op Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokkeer Veelvoorkomende Kwetsbaarheden\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache Assets\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Pad Behouden\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocollen\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets Ondersteuning\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Poort Doorsturen\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Alleen HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Laden…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Inloggen\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Aangepaste Nginx Configuratie\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Voeg jouw aangepaste Nginx configuratie hier op eigen risico toe!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Jij hebt geen toegang om dit te bekijken.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Thuis\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"De pagina waar je naar op zoek bent kan niet worden gevonden\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oeps… Je hebt een foutpagina gevonden\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Fout\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} is verwijderd\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} is uitgezet\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} is aangezet\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} is vernieuwd\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} is opgeslagen\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Succes\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Voeg {object} toe\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Verwijder {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Weet je zeker dat je {object} wilt verwijderen?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Bewerk {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Er zijn geen {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} is aangemaakt\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} is verwijderd\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} is uitgezet\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} is aangezet\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} is vernieuwd\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} is bijgewerkt\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opties\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Wachtwoord\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Willekeurig wachtwoord genereren\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Wachtwoord Verbergen\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Toon Wachtwoord\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Verborgen\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Beheer\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Alleen Bekijken\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Alle Items\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Item Zichtbaarheid\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Alleen Aangemaakte Items\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Hostname / IP Doorsturen\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Openbaar\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Redirection Host\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Doorgestuurd Domein\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Redirection Hosts\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Beheerder\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standaard Gebruiker\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Opslaan\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Instelling\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Instellingen\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Standaard Site\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 Pagina\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Geen Antwoord (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Felicitatiepagina\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Wat te tonen als Nginx een onbekende Host ontvangt\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Aangepaste HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Plaats jouw aangepaste HTML hier -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Omleiding\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Begin met het aanmaken van je beheerder account.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Welkom!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Inloggen\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL Certificaten\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Doorgestuurde Host\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Inkomende Poort\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Update Beschikbaar: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Gebruiker\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Verander Wachtwoord\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Bevestig Wachtwoord\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Huidig Wachtwoord\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Profiel Bewerken\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Volledige Naam\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Inloggen als {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Uitloggen\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nieuw Wachtwoord\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Bijnaam\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Zet Wachtwoord\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Zet machtigingen voor {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Verander naar donkere modus\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Verander naar lichte modus\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Gebruikersnaam\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Gebruikers\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/no.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Gjenstående backup-koder: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Lagre disse backup-kodene på et sikkert sted. Hver kode kan kun brukes én gang.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Deaktiver tofaktorautentisering\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"Deaktiver 2FA\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"Å deaktivere tofaktorautentisering vil gjøre kontoen din mindre sikker.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Deaktivert\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"Jeg har lagret backup-kodene mine\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Aktiver tofaktorautentisering\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Aktivert\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Angi verifiseringskode\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Angi verifiseringskode for å deaktivere\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Regenerer\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Generer nye backup-koder\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Angi en verifiseringskode for å generere nye backup-koder. Dine gamle koder vil bli ugyldige.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Hemmelig nøkkel\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Skann denne QR-koden med autentiseringsappen din, eller skriv inn nøkkelen manuelt.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Tofaktorautentisering\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Verifiser og aktiver\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Tilgangsliste\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {regel} other {regler}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {bruker} other {brukere}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Når minst én regel finnes, legges denne \\\"deny all\\\"-regelen til sist\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Merk at tillat- og nekt-direktivene brukes i den rekkefølgen de er definert.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Send autentisering til upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Offentlig tilgjengelig\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Ingen grunnleggende autentisering kreves\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 eller 192.168.1.0/24 eller 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Oppfyll en av kravene\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {bruker} other {brukere}}, {rules} {rules, plural, one {regel} other {regler}} - Opprettet: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Tilgangslister\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Legg til\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Legg til plassering\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Tillat\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Lukk\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Slett\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Nekt\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Deaktiver\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Last ned\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Rediger\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aktiver\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Tillatelser\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Forny\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Vis detaljer\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Revisjonslogger\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Auto\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Avbryt\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Sertifikat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Egendefinert Sertifikat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Egendefinert Sertifikat nøkkel\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Egendefinert Intermediate Sertifikat\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"I bruk\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Ingen sertifikat tildelt\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Denne verten vil ikke bruke HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Ingen\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Ikke i bruk\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Forny sertifikat\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Sertifikater\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Egendefinert Sertifikat\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Nøkkelfiler beskyttet med passordfrase støttes ikke.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Innhold i legitimasjonsfil\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Denne pluginen krever en konfigurasjonsfil som inneholder en API-token eller andre legitimasjoner for leverandøren din\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Disse dataene vil bli lagret som ren tekst i databasen og i en fil!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propageringsekunder\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"La stå tomt for å bruke pluginens standardverdi. Antall sekunder å vente på DNS-propagasjon.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS-leverandør\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Velg en leverandør...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Det finnes en server på dette domenet, men det ser ikke ut til å være Nginx Proxy Manager. Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Det finnes ingen server tilgjengelig på dette domenet. Vennligst sørg for at domenet ditt eksisterer og peker til IP-en der NPM-instansen kjører, og om nødvendig at port 80 er videresendt i ruteren din.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Serveren din er tilgjengelig, og det bør være mulig å opprette sertifikater.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Det finnes en server på dette domenet, men den returnerte en uventet statuskode {code}. Er det NPM-serveren? Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Det finnes en server på dette domenet, men den returnerte uventet data. Er det NPM-serveren? Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Testresultater\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Disse domenene må allerede være konfigurert til å peke til denne installasjonen.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Nøkkeltype\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA er bredt kompatibel, ECDSA er raskere og mer sikker, men støttes kanskje ikke av eldre systemer\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"med Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Be om et nytt sertifikat\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Tilgang\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorisasjon\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorisasjoner\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Egendefinerte plasseringer\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destinasjon\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Detaljer\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"E-post\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Hendelse\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Utløper\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP-kode\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Innkommende port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Navn\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokoll\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Leverandør\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roller\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Regler\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Oppfylle\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Alle\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Noen\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Skjema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Kilde\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Opprettet: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Dashboard\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Tjener ikke funnet\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Tjenere ikke funnet\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Tjener} other {404 Tjenere}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Deaktivert\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Domener\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} domener maksimum\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Begynn å skrive for å legge til domene...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards er ikke tillatt for denne typen\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards støttes ikke for denne CA-en\"\n\t},\n\t\"domains.advanced\": {\n\t\t\"defaultMessage\": \"Avansert\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Tving SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Aktivert\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Underdomener\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Støtte\"\n\t},\n\t\"domains.trust-forwarded-proto\": {\n\t\t\"defaultMessage\": \"Stol på Upstream Forwarded Proto Headers\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Bruk DNS Utfordring\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"E-postadresse\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Ingen resultater funnet\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Hvorfor ikke opprette en?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Aktivert\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Enten en autorisasjon eller en tilgangsregel er påkrevd\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Autorisasjonsbrukernavn må være unike\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Ugyldig e-post eller passord\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Ugyldig domene: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Ugyldig e-postadresse\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maksimal lengde er {max} tegn\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"For mange domener, maks er {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maksimum er {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimum lengde er {min} tegn\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum er {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Passordene må være like\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Dette er påkrevd\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Utløper: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork meg på Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokker vanlige utnyttelser\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Mellomlagre ressurser\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Behold sti\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokoller\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets-støtte\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Viderekoble Port\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Skjema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Vertsnavn\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Kun HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Laster…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Verifikasjonskode\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Skriv inn kode\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Skriv inn koden fra autentiseringsappen din\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"To-faktorautentisering\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Verifiser\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Logg på kontoen din\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Egendefinert Nginx-konfigurasjon\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Skriv inn din egendefinerte Nginx-konfigurasjon her på egen risiko!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Du har ikke tilgang til å se dette.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Ta meg hjem\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Beklager, siden du leter etter ble ikke funnet\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… Du har nettopp funnet en feilsiden\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Feil\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} har blitt slettet\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} har blitt deaktivert\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} har blitt aktivert\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} har blitt fornyet\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} har blitt lagret\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Suksess\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Legg til {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Slett {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Er du sikker på at du vil slette dette {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Rediger {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Det finnes ingen {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Opprettet {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Slettet {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Deaktivert {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Aktivert {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Fornyet {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Oppdatert {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Utilgjengelig\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Tilgjengelig\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Alternativer\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Passord\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Generer tilfeldig passord\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Skjul passord\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Vis passord\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Skjult\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Administrer\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Kun visning\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Alle elementer\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Element Synlighet\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Kun opprettede elementer\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Forward Hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy-verter\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy-vert} other {Proxy-verter}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Offentlig\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Omdirigeringsvert\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Viderekoble domene\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP-kode\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Omdirigeringsverter\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Omdirigeringsvert} other {Omdirigeringsverter}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Multiple Choices\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Flyttet permanent\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Flyttet midlertidig\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Se andre\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Midlertidig omdirigering\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Permanent omdirigering\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standardbruker\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Lagre\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Innstilling\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Innstillinger\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Standardnettsted\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404-side\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Ingen respons (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Gratulerer-side\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Hva som skal vises når Nginx treffes med en ukjent vert\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Egendefinert HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Skriv inn ditt egendefinerte HTML-innhold her -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Omdiriger\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Kom i gang ved å opprette din administratorkonto.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Velkommen!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Logg inn\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL-sertifikat\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Strøm\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Viderekoble vert\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com eller 10.0.0.1 eller 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Innkommende port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Strømmer\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Strøm} other {Strømmer}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Oppdatering tilgjengelig: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Bruker\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Endre passord\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Bekreft passord\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Nåværende passord\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Rediger profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Fullt navn\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Logg inn som {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Logg ut\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nytt passord\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Kallenavn\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Angi passord\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Angi tillatelser for {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Bytt til mørk modus\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Bytt til lys modus\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"To-faktor autentisering\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Brukernavn\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Brukere\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/pl.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"wpis listy dostępu\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Reguła} few {Reguły} other {Reguł}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Użytkownik} few {Użytkownicy} other {Użytkowników}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Gdy istnieje co najmniej 1 reguła, ta reguła blokująca wszystko zostanie dodana na końcu\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Należy pamiętać, że dyrektywy zezwolenia i odmowy będą stosowane w kolejności, w jakiej zostały zdefiniowane.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Przekaż uwierzytelnienie do serwera docelowego\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Publicznie dostępne\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Nie wymaga uwierzytelnienia podstawowego\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Spełnij dowolny warunek\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Użytkownik} few {Użytkowników} other {Użytkowników}}, {rules} {rules, plural, one {Reguła} few {Reguły} other {Reguł}} - Utworzono: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Listy dostępu\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Dodaj\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Dodaj lokalizację\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Zezwól\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Zamknij\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Usuń\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Odrzuć\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Wyłącz\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Pobierz\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Edytuj\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Włącz\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Uprawnienia\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Odnów\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Pokaż szczegóły\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Logi\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Anuluj\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"certyfikat\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certyfikat\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Klucz certyfikatu\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Certyfikat pośredni\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"W użyciu\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Nie przypisano certyfikatu\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Ten host nie będzie używał HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Brak\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Nie używany\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Odnów certyfikat\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certyfikaty\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Własny certyfikat\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Pliki kluczy chronione hasłem nie są obsługiwane.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Zawartość pliku z poświadczeniami\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Ta wtyczka wymaga pliku konfiguracyjnego zawierającego token API lub inne poświadczenia dla twojego dostawcy\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Te dane zostaną zapisane jako zwykły tekst w bazie danych i pliku!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Sekundy propagacji\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Pozostaw puste, aby użyć domyślnej wartości wtyczki. Liczba sekund oczekiwania na propagację DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Dostawca DNS\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Znaleziono serwer pod tą domeną, ale nie wygląda na to, że jest to Nginx Proxy Manager. Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Brak dostępnego serwera pod tą domeną. Upewnij się, że twoja domena istnieje i wskazuje na adres IP, gdzie działa twoja instancja NPM, oraz w razie potrzeby, że port 80 jest przekierowany w routerze lub owarty w firewall-u.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Twój serwer jest dostępny i tworzenie certyfikatów powinno być możliwe.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Znaleziono serwer pod tą domeną, ale zwrócił nieoczekiwany kod statusu {code}. Czy to serwer NPM? Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Znaleziono serwer pod tą domeną, ale zwrócił nieoczekiwane dane. Czy to serwer NPM? Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Wyniki testu\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Typ klucza\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA jest szeroko kompatybilny, ECDSA jest szybszy i bezpieczniejszy, ale może nie być obsługiwany przez starsze systemy\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"z Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Wygeneruj nowy certyfikat\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Dostęp\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autoryzacja\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autoryzacje\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Własne ustawienia lokalizacji\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Cel\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Szczegóły\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Zdarzenie\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Wygasa\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Kod HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Port przychodzący\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nazwa\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokół\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Dostawca\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Rola\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Reguły\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Spełnij\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Wszystkie\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Dowolny\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schemat\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Źródło\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Status\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Utworzono: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Panel\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"host 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {host 404} few {hosty 404} other {hostów 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Wyłączone\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Nazwy domen\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maksymalnie {count} nazw domen\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Zacznij pisać, aby dodać domenę...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Symbole wieloznaczne nie są dozwolone dla tego typu\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Symbole wieloznaczne nie są obsługiwane dla tego CA\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Wymuś SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"Włącz HSTS \"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS dla subdomen\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Obsługa HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Użyj wyzwania DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Adres email\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nie znaleziono wyników\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Może utworzysz nowy?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Włączone\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Wymagana jest co najmniej jedna autoryzacja lub jedna reguła dostępu\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Nazwy użytkowników autoryzacji muszą być unikalne\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Nieprawidłowy email lub hasło\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Nieprawidłowa domena: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Nieprawidłowy adres email\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maksymalna długość to {max} {max, plural, one {znak} few {znaki} other {znaków}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Zbyt wiele domen, maksimum to {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maksimum to {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimalna długość to {min} {min, plural, one {znak} few {znaki} other {znaków}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum to {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Hasła muszą się zgadzać\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"To pole jest wymagane\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Wygasa: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forkuj mnie na Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokuj typowe exploity\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Buforuj zasoby statyczne (ang. cache)\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Zachowaj ścieżkę\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokoły\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Obsługa WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port docelowy\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schemat\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosty\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Tylko HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt przez DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt przez HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Ładowanie…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Zaloguj się na swoje konto\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Własna konfiguracja Nginx\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Wprowadź tutaj własną konfigurację Nginx na własną odpowiedzialność!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Nie masz uprawnień do wyświetlenia tego.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Zabierz mnie do strony głównej\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Przepraszamy, ale strona, której szukasz, nie została znaleziona\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Ups… Właśnie znalazłeś stronę błędu\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Błąd\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} został usunięty\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} został wyłączony\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} został włączony\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} został odnowiony\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} został zapisany\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Sukces\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Nowy {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Usuń {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Czy na pewno chcesz usunąć {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Edytuj {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Brak {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Utworzono {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Usunięto {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Wyłączono {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Włączono {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Odnowiono {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Zaktualizowano {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opcje\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Hasło\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Wygeneruj losowe hasło\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Ukryj hasło\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Pokaż hasło\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Ukryte\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Zarządzaj\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Tylko podgląd\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Wszystkie elementy\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Widoczność elementów\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Tylko utworzone elementy\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"host proxy\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Przekieruj na hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {host proxy} few {hosty proxy} many {hostów proxy} other {hostów proxy}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Publiczne\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"adres przekierowania\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Domena docelowa\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Kod HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Przekierowania\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {przekierowanie} few {przekierowania} many {przekierowań} other {przekierowań}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrator\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standardowy użytkownik\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Zapisz\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Ustawienie\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Ustawienia\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Domyślna strona\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Strona 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Brak odpowiedzi (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Strona gratulacyjna\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Co wyświetlić, gdy Nginx otrzyma nieznany Host\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Własny HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Wprowadź tutaj własną zawartość HTML -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Przekierowanie\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Zacznij od utworzenia konta administratora.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Witaj!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Zaloguj się\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Certyfikat SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"strumień\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Host docelowy\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Port przychodzący\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Strumienie\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {strumień} few {strumienie} many {strumieni} other {strumieni}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"użytkownik\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Zmień hasło\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Potwierdź nowe hasło\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Aktualne hasło\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Edytuj profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Imię / Nazwisko\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Zaloguj jako {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Wyloguj\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nowe hasło\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Pseudonim\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Ustaw hasło\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Ustaw uprawnienia dla {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Przełącz na tryb ciemny\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Przełącz na tryb jasny\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nazwa użytkownika\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Użytkownicy\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/pt.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Lista de Controlo de Acesso (ACL)\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Regra} other {Regras}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Utilizador} other {Utilizadores}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Quando existir pelo menos 1 regra, esta regra de negação geral será aplicada em último lugar\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Nota: as diretivas allow e deny são aplicadas pela ordem em que forem definidas.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Passar Autenticação para o Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Acesso Público\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Sem autenticação básica\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 ou 192.168.1.0/24 ou 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Satisfazer Qualquer\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Utilizador} other {Utilizadores}}, {rules} {rules, plural, one {Regra} other {Regras}} – Criado em: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Listas de Controlo de Acesso (ACL)\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Adicionar\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Adicionar Location\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Permitir\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Fechar\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Eliminar\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Negar\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Desativar\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Descarregar\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Editar\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Ativar\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Permissões\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Renovar\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Ver Detalhes\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Registos de Auditoria\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Automático\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Cancelar\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Certificado\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificado Personalizado\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Chave do Certificado\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Certificado Intermédio\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Em Utilização\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Nenhum certificado atribuído\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Este host não irá utilizar HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Nenhum\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Não Utilizado\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Renovar Certificado\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certificados\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Certificado Personalizado\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Ficheiros de chave protegidos por palavra-passe não são suportados.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Conteúdo do Ficheiro de Credenciais\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Este plugin requer um ficheiro de configuração contendo um token API ou outras credenciais do fornecedor DNS.\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Estes dados serão guardados em texto simples na base de dados e num ficheiro!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Segundos de Propagação\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Deixe em branco para usar o valor predefinido do plugin. Número de segundos a aguardar pela propagação DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Fornecedor DNS\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Selecionar fornecedor...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Foi encontrado um servidor neste domínio, mas não parece ser o Nginx Proxy Manager. Certifique-se de que o domínio aponta para o IP onde a sua instância está a correr.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Não existe nenhum servidor acessível neste domínio. Certifique-se de que o domínio existe, aponta para o IP correto e que a porta 80 está encaminhada no seu router.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"O servidor está acessível e a criação de certificados deverá ser possível.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Foi encontrado um servidor neste domínio, mas devolveu um código inesperado ({code}). Será o servidor NPM? Confirme que o domínio aponta para o IP correto.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Foi encontrado um servidor neste domínio, mas devolveu dados inesperados. Será o servidor NPM? Confirme que o domínio aponta para o IP correto.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Resultados do Teste\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Estes domínios devem já estar configurados para apontar para esta instalação.\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"com o Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Pedir Novo Certificado\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Acesso\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorização\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorizações\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Locations Personalizados\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Destino\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Detalhes\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Evento\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Expira\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Código HTTP\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Porta de Entrada\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Nome\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protocolo\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Fornecedor\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Funções\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Regras\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Satisfazer\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Todos\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Qualquer\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Esquema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Origem\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Estado\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Criado em: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Painel\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Host 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Hosts 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host 404} other {Hosts 404}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Desativado\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Nomes de Domínio\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Máximo de {count} domínios\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Comece a escrever para adicionar um domínio...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards não permitidos para este tipo\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards não suportados por esta AC\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Forçar SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Ativado\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS para Subdomínios\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Suporte HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Utilizar DNS Challenge\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Endereço de Email\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nenhum resultado encontrado\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Porque não cria um?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Ativado\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"É necessária pelo menos uma Autorização ou uma Regra de Acesso\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Os nomes de utilizador de autorização devem ser únicos\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Email ou palavra-passe inválidos\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Domínio inválido: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Endereço de email inválido\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Tamanho máximo: {max} caractere{max, plural, one {} other {s}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Demasiados domínios; o máximo é {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Máximo permitido: {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Tamanho mínimo: {min} caractere{min, plural, one {} other {s}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Mínimo permitido: {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"As palavras-passe têm de coincidir\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Campo obrigatório\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Expira em: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Faz fork no GitHub\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Bloquear Exploits Comuns\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache de Conteúdos Estáticos\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Preservar Caminho\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protocolos\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Suporte para WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Porta de Encaminhamento\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Esquema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hosts\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Apenas HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt via HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"A carregar…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Iniciar sessão na sua conta\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Configuração Nginx Personalizada\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Insira aqui a sua configuração Nginx personalizada (utilize por sua conta e risco!)\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Não tem permissões para ver esta página.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Voltar à página inicial\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"A página que procura não foi encontrada.\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Oops… Encontrou uma página de erro\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Erro\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} foi eliminado\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} foi desativado\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} foi ativado\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} foi renovado\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} foi guardado\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Sucesso\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Adicionar {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Eliminar {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Tem a certeza de que deseja eliminar este {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Editar {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Não existem {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} criado\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} eliminado\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} desativado\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} ativado\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} renovado\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} atualizado\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Opções\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Palavra-passe\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Gerar palavra-passe aleatória\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Esconder Palavra-passe\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Mostrar Palavra-passe\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Oculto\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Gerir\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Apenas Visualização\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Todos os Itens\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Visibilidade do Item\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Apenas Itens Criados\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Hostname/IP de Encaminhamento\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Hosts\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Público\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Host de Redirecionamento\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Domínio de Destino\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"Código HTTP\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Hosts de Redirecionamento\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Host de Redirecionamento} other {Hosts de Redirecionamento}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Múltiplas Escolhas\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Movido Permanentemente\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Movido Temporariamente\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Ver Outro\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Redirecionamento Temporário\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Redirecionamento Permanente\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrador\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Utilizador Comum\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Guardar\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Definição\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Definições\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Site Predefinido\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Página 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Sem Resposta (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Página de Boas-vindas\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"O que apresentar quando o Nginx recebe um Host desconhecido\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML Personalizado\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Insira aqui o seu conteúdo HTML personalizado -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Redirecionar\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Comece por criar a sua conta de administrador.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Bem-vindo!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Iniciar Sessão\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Certificado SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Host de Destino\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com ou 10.0.0.1 ou 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Porta de Entrada\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streams\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Stream} other {Streams}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Testar\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Atualização Disponível: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Utilizador\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Alterar Palavra-passe\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Confirmar Palavra-passe\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Palavra-passe Atual\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Editar Perfil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Nome Completo\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Iniciar sessão como {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Terminar Sessão\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nova Palavra-passe\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Alcunha\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Definir Palavra-passe\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Definir Permissões para {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Ativar Modo Escuro\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Ativar Modo Claro\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Nome de Utilizador\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Utilizadores\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/ru.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Список доступа\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {правило} few {правила} many {правил} other {правила}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Если есть хотя бы одно правило, правило 'запретить всё' будет добавлено последним\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Обратите внимание: разрешающие и запрещающие директивы применяются в порядке их определения.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Передавать авторизацию на upstream-сервер\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Общедоступный\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Без аутентификации\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Любое совпадение\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}, {rules} {rules, plural, one {правило} few {правила} many {правил} other {правила}} - создан: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Списки доступа\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Добавить\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Добавить маршрут\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Закрыть\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Удалить\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Выключить\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Скачать\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Изменить\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Включить\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Разрешения\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Продлить\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Просмотреть сведения\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Журнал аудита\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Отменить\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Сертификат\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Сертификат\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Ключ сертификата\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Промежуточный сертификат\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Используется\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Сертификат не назначен\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Этот хост не будет использовать HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Нет\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Не используется\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Продлить сертификат\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Сертификаты\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Свой сертификат\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Файлы ключей, защищённые паролем, не поддерживаются.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Содержимое файла учётных данных\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Этот плагин требует файл конфигурации, содержащий API-токен или другие учётные данные вашего провайдера\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Эти данные будут храниться в незашифрованном виде в базе данных и файле!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Ожидание распространения (сек.)\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Оставьте пустым для значения по умолчанию плагина. Секунды ожидания распространения DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS-провайдер\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"На этом домене найден сервер, но, похоже, это не Nginx Proxy Manager. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Не удалось проверить доступность из‑за ошибки связи с site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"На этом домене недоступен сервер. Убедитесь, что домен существует и указывает на IP-адрес, где запущен ваш экземпляр NPM, и при необходимости порт 80 проброшен на вашем роутере.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Сервер доступен, выпуск сертификатов возможен.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"На этом домене найден сервер, но он вернул неожиданный статус‑код {code}. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"На этом домене найден сервер, но он вернул неожиданные данные. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Результаты проверки\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Эти домены должны быть настроены и указывать на этот экземпляр.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Тип ключа\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA широко совместим, ECDSA быстрее и безопаснее, но может не поддерживаться старыми системами\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"через Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Получить новый сертификат\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Доступ\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Авторизация\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Авторизации\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Свои маршруты\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Назначение\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Сведения\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Эл. почта\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Событие\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Истекает\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP-код\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Входящий порт\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Имя\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Протокол\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Провайдер\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Роли\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Правила\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Условия\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Все\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Любое\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Схема\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Источник\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Статус\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Создан: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Обзор\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404-хост\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404-хосты\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404-хост} few {404-хоста} many {404-хостов} other {404-хоста}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Выключен\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Домены\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Максимум {count} доменов\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Начните ввод, чтобы добавить домен...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Подстановочные домены не разрешены для этого типа\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Подстановочные домены не поддерживаются этим центром сертификации\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Всегда SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"Поддержка HSTS\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"Поддомены HSTS\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Поддержка HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Проверка через DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Адрес эл. почты\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Ничего не найдено\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Почему бы не создать его?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Включён\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Требуется хотя бы одна авторизация или одно правило доступа\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Имена пользователей для авторизации должны быть уникальными\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Неверный адрес эл. почты или пароль\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Неверный домен: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Неверный адрес эл. почты\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Максимальная длина {max} {max, plural, one {символ} few {символа} many {символов} other {символа}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Слишком много доменов, максимум {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Максимум {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Минимальная длина {min} {min, plural, one {символ} few {символа} many {символов} other {символа}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Минимум {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Пароли должны совпадать\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Обязательное поле\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Истекает: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Сделать форк на GitHub\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Блокировать известные эксплойты\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Кэшировать ресурсы\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Сохранять путь\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Протоколы\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Поддержка WebSocket\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Порт перенаправления\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Схема\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Хосты\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Только HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt через DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt через HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Загрузка…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Авторизация\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Своя Nginx-конфигурация\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Введите здесь свою Nginx-конфигурацию, будьте осторожны!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"У вас нет доступа для просмотра.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Вернуться на главную\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Извините, но страница, которую вы ищете, не найдена\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Упс… Вы попали на страницу ошибки\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Ошибка\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} удален\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} выключен\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} включен\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} продлен\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} сохранен\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Успешно\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Добавить {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Удалить {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Вы уверены, что хотите удалить {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Изменить {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"{objects} отсутствуют\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} создан\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} удален\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} выключен\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} включен\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} продлен\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} обновлен\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Офлайн\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Онлайн\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Параметры\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Пароль\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Сгенерировать случайный пароль\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Скрыть пароль\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Показать пароль\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Скрыто\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Управление\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Только просмотр\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Все элементы\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Видимость элементов\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Созданные элементы\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Прокси-хост\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Хост / IP перенаправления\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Прокси-хосты\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {прокси-хост} few {прокси-хоста} many {прокси-хостов} other {прокси-хоста}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Общедоступный\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Редирект-хост\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Домен перенаправления\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP-код\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Редирект-хосты\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {редирект-хост} few {редирект-хоста} many {редирект-хостов} other {редирект-хоста}}\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Администратор\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Обычный пользователь\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Сохранить\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Настройка\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Настройки\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Страница по умолчанию\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404-страница\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Нет ответа (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Страница поздравления\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Что показывать, когда Nginx получает неизвестный хост\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Свой HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Введите здесь свой HTML-контент -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Перенаправление\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Начните с создания учётной записи администратора.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Добро пожаловать!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Войти\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL-сертификат\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Поток\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Хост перенаправления\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Входящий порт\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Потоки\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {поток} few {потока} many {потоков} other {потока}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Проверить\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Пользователь\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Изменить пароль\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Повторите пароль\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Текущий пароль\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Изменить профиль\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Полное имя\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Войти как {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Выйти\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Новый пароль\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Псевдоним\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Задать пароль\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Задать разрешения для {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Включить тёмную тему\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Включить светлую тему\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Имя пользователя\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Пользователи\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/sk.json",
    "content": "{\n\t\"2fa.backup-codes-remaining\": {\n\t\t\"defaultMessage\": \"Počet zostávajúcich záložných kódov: {count}\"\n\t},\n\t\"2fa.backup-warning\": {\n\t\t\"defaultMessage\": \"Tieto záložné kódy si uložte na bezpečnom mieste. Každý kód je možné použiť len raz.\"\n\t},\n\t\"2fa.disable\": {\n\t\t\"defaultMessage\": \"Vypnúť dvojfaktorové overovanie\"\n\t},\n\t\"2fa.disable-confirm\": {\n\t\t\"defaultMessage\": \"Vypnúť 2FA\"\n\t},\n\t\"2fa.disable-warning\": {\n\t\t\"defaultMessage\": \"Vypnutím dvojfaktorového overovania sa zníži bezpečnosť vášho účtu.\"\n\t},\n\t\"2fa.disabled\": {\n\t\t\"defaultMessage\": \"Vypnuté\"\n\t},\n\t\"2fa.done\": {\n\t\t\"defaultMessage\": \"Uložil som si svoje záložné kódy.\"\n\t},\n\t\"2fa.enable\": {\n\t\t\"defaultMessage\": \"Zapnúť dvojfaktorové overovanie\"\n\t},\n\t\"2fa.enabled\": {\n\t\t\"defaultMessage\": \"Zapnuté\"\n\t},\n\t\"2fa.enter-code\": {\n\t\t\"defaultMessage\": \"Zadajte overovací kód\"\n\t},\n\t\"2fa.enter-code-disable\": {\n\t\t\"defaultMessage\": \"Zadajte overovací kód na vypnutie\"\n\t},\n\t\"2fa.regenerate\": {\n\t\t\"defaultMessage\": \"Znova vytvoriť\"\n\t},\n\t\"2fa.regenerate-backup\": {\n\t\t\"defaultMessage\": \"Znova vytvoriť záložné kódy\"\n\t},\n\t\"2fa.regenerate-instructions\": {\n\t\t\"defaultMessage\": \"Zadajte overovací kód, aby sa vytvorili nové záložné kódy. Vaše staré kódy budú neplatné.\"\n\t},\n\t\"2fa.secret-key\": {\n\t\t\"defaultMessage\": \"Tajný kľúč\"\n\t},\n\t\"2fa.setup-instructions\": {\n\t\t\"defaultMessage\": \"Naskenujte tento QR kód pomocou svojej overovacej aplikácie alebo zadajte tajný kľúč ručne.\"\n\t},\n\t\"2fa.status\": {\n\t\t\"defaultMessage\": \"Stav\"\n\t},\n\t\"2fa.title\": {\n\t\t\"defaultMessage\": \"Dvojfaktorové overenie\"\n\t},\n\t\"2fa.verify-enable\": {\n\t\t\"defaultMessage\": \"Overiť a zapnúť\"\n\t},\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"zoznam prístupov\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {pravidlo} few {pravidlá} other {pravidiel}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {používateľ} few {používatelia} other {používateľov}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Keď existuje aspoň jedno pravidlo, toto pravidlo „zamietnuť všetko“ bude pridané ako posledné\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Upozornenie: pravidlá povoliť a zamietnuť budú uplatňované v poradí, v akom sú definované.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Odoslať overenie na Upstream\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Verejne prístupné\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Nie je potrebné základné overenie\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 alebo 192.168.1.0/24 alebo 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Splniť ktorékoľvek\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {používateľ} few {používatelia} other {používateľov}}, {rules} {rules, plural, one {pravidlo} few {pravidlá} other {pravidiel}} - Vytvorené: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Zoznamy prístupov\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Pridať\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Pridať umiestnenie\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"Povoliť\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Zavrieť\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Vymazať\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Zamietnuť\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Deaktivovať\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Stiahnuť\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Upraviť\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Aktivovať\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Oprávnenia\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Obnoviť\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Zobraziť podrobnosti\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Záznamy auditu\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Automaticky\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Zrušiť\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"certifikát\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certifikát\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Kľúč certifikátu\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Sprostredkovateľský certifikát\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Používa sa\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Nie je priradený žiadny certifikát\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Tento hostiteľ nebude používať HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Žiadny\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Nepoužíva sa\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Obnoviť certifikát\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Certifikáty\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Vlastný certifikát\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Súbory kľúčov chránené heslom nie sú podporované.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Obsah súboru s prihlasovacími údajmi\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Tento doplnok vyžaduje konfiguračný súbor obsahujúci API token alebo iné prihlasovacie údaje vášho poskytovateľa\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Tieto údaje budú uložené v databáze a v súbore ako obyčajný text!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Propagácia v sekundách\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Ponechajte prázdne pre predvolenú hodnotu doplnku. Počet sekúnd, počas ktorých sa čaká na propagáciu DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS poskytovateľ\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Vyberte poskytovateľa...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Táto sekcia vyžaduje znalosť Certbotu a jeho DNS doplnkov. Prosím, pozrite si dokumentáciu príslušného doplnku.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Na tejto doméne bol nájdený server, ale nezdá sa, že ide o Nginx Proxy Manager. Uistite sa, že vaša doména smeruje na IP, kde beží vaša inštancia NPM.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Na tejto doméne nie je dostupný žiadny server. Uistite sa, že doména existuje a smeruje na IP adresu s NPM a ak je to potrebné, port 80 je presmerovaný vo vašom smerovači.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Váš server je dostupný a vytvorenie certifikátu by malo byť možné.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Na tejto doméne bol nájdený server, ale vrátil neočakávaný stavový kód {code}. Je to NPM server? Uistite sa prosím, že doména smeruje na IP, kde beží vaša inštancia NPM.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Na tejto doméne bol nájdený server, ale vrátil neočakávané údaje. Je to NPM server? Uistite sa, že doména smeruje na IP, kde beží vaša inštancia NPM.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Výsledky testu\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Tieto domény musia byť už nakonfigurované tak, aby smerovali na túto inštaláciu.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Typ kľúča\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA je široko kompatibilný, ECDSA je rýchlejší a bezpečnejší, ale nemusí byť podporovaný staršími systémami\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"pomocou Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Vyžiadať nový certifikát\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Prístup\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Autorizácia\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Autorizácie\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Vlastné umiestnenia\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Cieľ\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Podrobnosti\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Udalosť\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Platnosť do\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"Prístup\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Vstupný port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Názov\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Poskytovateľ\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roly\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Pravidlá\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Splniť\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Všetky\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Ktorékoľvek\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Zdroj\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Stav\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Vytvorené: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Panel\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 hostiteľa\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Hostitelia\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 hostiteľ} few {404 hostitelia} other {404 hostiteľov}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Deaktivované\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Doménové mená\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maximálne {count} doménových mien\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Začnite písať pre pridanie domény...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Wildcards nie sú pre tento typ povolené\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Wildcards nie sú podporované pre túto certifikačnú autoritu\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Vynútiť SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS povolené\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS pre subdomény\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Podpora HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Použiť DNS výzvu\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Emailová adresa\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Nenašli sa žiadne výsledky\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Prečo nevytvoríte nejaký?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Aktivované\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Je vyžadovaná aspoň jedna autorizácia alebo jedno prístupové pravidlo\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Používateľské mená pre autorizáciu musia byť jedinečné\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Neplatný email alebo heslo\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Neplatná doména: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Neplatná emailová adresa\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maximálna dĺžka je {max} znak{max, plural, one {} few {y} other {ov}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Príliš veľa domén, maximum je {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maximum je {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimálna dĺžka je {min} znak{min, plural, one {} few {y} other {ov}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum je {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Heslá sa musia zhodovať\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Toto pole je povinné\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Platnosť do: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Forknite ma na GitHube\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Blokovať bežné exploity\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Uložiť zdroje do vyrovnávacej pamäte\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Zachovať cestu\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokoly\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Podpora WebSockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Port presmerovania\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Schéma\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Hostitelia\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Len HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt cez DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt cez HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Načítava sa…\"\n\t},\n\t\"login.2fa-code\": {\n\t\t\"defaultMessage\": \"Overovací kód\"\n\t},\n\t\"login.2fa-code-placeholder\": {\n\t\t\"defaultMessage\": \"Vložiť kód\"\n\t},\n\t\"login.2fa-description\": {\n\t\t\"defaultMessage\": \"Vložte kód z vašej overovacej aplikácie\"\n\t},\n\t\"login.2fa-title\": {\n\t\t\"defaultMessage\": \"Dvoj-faktorové overenie\"\n\t},\n\t\"login.2fa-verify\": {\n\t\t\"defaultMessage\": \"Overiť\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Prihláste sa do svojho účtu\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Vlastná Nginx konfigurácia\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Zadajte vlastnú Nginx konfiguráciu na vlastné riziko!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Nemáte oprávnenie na zobrazenie tohto obsahu.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Späť na hlavnú stránku\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Ľutujeme, stránka, ktorú hľadáte, nebola nájdená\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Ups… Našli ste chybovú stránku\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Chyba\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} bol odstránený\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} bol deaktivovaný\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} bol aktivovaný\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} bol obnovený\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} bol uložený\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Úspech\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Pridať {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Vymazať {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Naozaj chcete vymazať tento {object}?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Upraviť {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Nie sú {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Vytvorený {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Vymazaný {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Deaktivovaný {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Aktivovaný {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Obnovený {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Aktualizovaný {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Offline\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Online\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Možnosti\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Heslo\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Vygenerovať náhodné heslo\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Skryť heslo\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Zobraziť heslo\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Skryté\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Spravovať\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Len na zobrazenie\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Všetky položky\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Viditeľnosť položky\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Len vytvorené položky\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"proxy hostiteľa\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Cieľový názov hostiteľa / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy hostitelia\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {proxy hostiteľ} few {proxy hostitelia} other {proxy hostiteľov}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Verejné\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"presmerovacieho hostiteľa\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Cieľová doména\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP kód\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Presmerovací hostitelia\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {presmerovací hostiteľ} few {presmerovací hostitelia} other {presmerovacích hostiteľov}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Viacero možností\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Trvalo presunuté\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Dočasne presunuté\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Pozrieť iné\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Dočasné presmerovanie\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Trvalé presmerovanie\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Administrátor\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Bežný používateľ\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Uložiť\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Nastavenie\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Nastavenia\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Predvolená stránka\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Stránka 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Bez odpovede (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Gratulačná stránka\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Čo zobraziť, keď Nginx zachytí neznámeho hostiteľa\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Vlastné HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Sem zadajte vlastný HTML obsah -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Presmerovať\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Začnite vytvorením administrátorského účtu.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Vitajte!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Prihlásiť sa\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL certifikát\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Cieľový hostiteľ\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"napriklad.sk alebo 10.0.0.1 alebo 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Vstupný port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Streamy\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {stream} few {streamy} other {streamov}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Dostupná aktualizácia: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"používateľa\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Zmeniť heslo\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Potvrdiť heslo\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Aktuálne heslo\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Upraviť profil\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Celé meno\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Prihlásiť sa ako {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Odhlásiť sa\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Nové heslo\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Prezývka\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Nastaviť heslo\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Nastaviť oprávnenia pre {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Prepnúť na tmavý režim\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Prepnúť na svetlý režim\"\n\t},\n\t\"user.two-factor\": {\n\t\t\"defaultMessage\": \"Dvojfakt. overenie\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Používateľské meno\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Používatelia\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/tr.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Erişim Listesi\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Kural} other {Kural}}\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Kullanıcı} other {Kullanıcı}}\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"En az 1 kural mevcut olduğunda, bu tümünü reddet kuralı en son eklenir\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"İzin ver ve reddet direktiflerinin tanımlandıkları sırayla uygulanacağını unutmayın.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Kimlik Doğrulamayı Yukarı Akışa İlet\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Herkese Açık\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Temel kimlik doğrulama gerekmez\"\n\t},\n\t\"access-list.rule-source.placeholder\": {\n\t\t\"defaultMessage\": \"192.168.1.100 veya 192.168.1.0/24 veya 2001:0db8::/32\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Herhangi Birini Karşıla\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} {users, plural, one {Kullanıcı} other {Kullanıcı}}, {rules} {rules, plural, one {Kural} other {Kural}} - Oluşturuldu: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Erişim Listeleri\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Ekle\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Konum Ekle\"\n\t},\n\t\"action.allow\": {\n\t\t\"defaultMessage\": \"İzin Ver\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Kapat\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Sil\"\n\t},\n\t\"action.deny\": {\n\t\t\"defaultMessage\": \"Reddet\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Devre Dışı Bırak\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"İndir\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Düzenle\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Etkinleştir\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"İzinler\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Yenile\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Detayları Görüntüle\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Denetim Kayıtları\"\n\t},\n\t\"auto\": {\n\t\t\"defaultMessage\": \"Otomatik\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"İptal\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Sertifika\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Sertifika\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Sertifika Anahtarı\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Ara Sertifika\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Kullanımda\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Sertifika atanmamış\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Bu host HTTPS kullanmayacak\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Yok\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Kullanılmıyor\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Sertifikayı Yenile\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Sertifikalar\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Özel Sertifika\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Parola ile korumalı anahtar dosyaları desteklenmiyor.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Kimlik Bilgileri Dosya İçeriği\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Bu eklenti, sağlayıcınız için bir API token'ı veya diğer kimlik bilgilerini içeren bir yapılandırma dosyası gerektirir\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Bu veriler veritabanında ve bir dosyada düz metin olarak saklanacak!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Yayılma Saniyesi\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Eklentinin varsayılan değerini kullanmak için boş bırakın. DNS yayılması için beklenilecek saniye sayısı.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS Sağlayıcı\"\n\t},\n\t\"certificates.dns.provider.placeholder\": {\n\t\t\"defaultMessage\": \"Bir Sağlayıcı Seçin...\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Bu bölüm Certbot ve DNS eklentileri hakkında bazı bilgiler gerektirir. Lütfen ilgili eklenti dokümantasyonuna bakın.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Bu alan adında bir sunucu bulundu ancak Nginx Proxy Manager gibi görünmüyor. Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Bu alan adında kullanılabilir bir sunucu yok. Lütfen alan adınızın mevcut olduğundan ve NPM örneğinizin çalıştığı IP'ye işaret ettiğinden ve gerekirse yönlendiricinizde 80 portunun yönlendirildiğinden emin olun.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Sunucunuz erişilebilir ve sertifika oluşturma mümkün olmalı.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Bu alan adında bir sunucu bulundu ancak beklenmeyen bir durum kodu döndürdü {code}. Bu NPM sunucusu mu? Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Bu alan adında bir sunucu bulundu ancak beklenmeyen veri döndürdü. Bu NPM sunucusu mu? Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Test Sonuçları\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Bu alan adları zaten bu kuruluma işaret edecek şekilde yapılandırılmış olmalıdır.\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"Let's Encrypt ile\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Yeni Sertifika İste\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Erişim\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Yetkilendirme\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Yetkilendirmeler\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Özel Konumlar\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Hedef\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Detaylar\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"E-posta\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Olay\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Sona Erer\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Kodu\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Gelen Port\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Ad\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Protokol\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Sağlayıcı\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Roller\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Kurallar\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Karşıla\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Tümü\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Herhangi Biri\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Şema\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Kaynak\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Durum\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Oluşturuldu: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Kontrol Paneli\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"404 Host\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"404 Host'lar\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {404 Host} other {404 Host}}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Devre Dışı\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Alan Adları\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Maksimum {count} alan adı\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Alan adı eklemek için yazmaya başlayın...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Bu tür için joker karakterler izin verilmez\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Bu CA için joker karakterler desteklenmiyor\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"SSL'i Zorla\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS Etkin\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS Alt Alan Adları\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 Desteği\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"DNS Challenge Kullan\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"E-posta adresi\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Sonuç bulunamadı\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Neden bir tane oluşturmuyorsunuz?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Etkin\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Ya bir Yetkilendirme ya da bir Erişim Kuralı gereklidir\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Yetkilendirme Kullanıcı Adları benzersiz olmalıdır\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Geçersiz e-posta veya şifre\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Geçersiz alan adı: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Geçersiz e-posta adresi\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Maksimum uzunluk {max} karakter{max, plural, one {} other {}}\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Çok fazla alan adı, maksimum {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Maksimum {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Minimum uzunluk {min} karakter{min, plural, one {} other {}}\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Minimum {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Şifreler eşleşmelidir\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Bu gereklidir\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Sona Erer: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Github'da Fork Yap\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Yaygın Saldırıları Engelle\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Varlıkları Önbelleğe Al\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Yolu Koru\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Protokoller\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets Desteği\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"İletme Portu\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Şema\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Host'lar\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"Sadece HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"DNS ile Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"HTTP ile Let's Encrypt\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Yükleniyor…\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Hesabınıza giriş yapın\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Özel Nginx Yapılandırması\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Kendi riskinizle özel Nginx yapılandırmanızı buraya girin!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Bunu görüntüleme erişiminiz yok.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Ana sayfaya götür\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Üzgünüz, aradığınız sayfa bulunamadı\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Hata… Bir hata sayfası buldunuz\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Hata\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} silindi\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} devre dışı bırakıldı\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} etkinleştirildi\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} yenilendi\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} kaydedildi\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Başarılı\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"{object} Ekle\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"{object} Sil\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Bu {object} öğesini silmek istediğinizden emin misiniz?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"{object} Düzenle\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Hiç {objects} yok\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"{object} oluşturuldu\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"{object} silindi\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"{object} devre dışı bırakıldı\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"{object} etkinleştirildi\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"{object} yenilendi\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"{object} güncellendi\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Çevrimdışı\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Çevrimiçi\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Seçenekler\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Şifre\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Rastgele şifre oluştur\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Şifreyi Gizle\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Şifreyi Göster\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Gizli\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Yönet\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Sadece Görüntüle\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Tüm Öğeler\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Öğe Görünürlüğü\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Sadece Oluşturulan Öğeler\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Proxy Host\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"İletme Host Adı / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Proxy Host'lar\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Proxy Host} other {Proxy Host}}\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Herkese Açık\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Yönlendirme Host'u\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"İletme Alan Adı\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Kodu\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Yönlendirme Host'ları\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Yönlendirme Host'u} other {Yönlendirme Host'u}}\"\n\t},\n\t\"redirection-hosts.http-code.300\": {\n\t\t\"defaultMessage\": \"300 Çoklu Seçenek\"\n\t},\n\t\"redirection-hosts.http-code.301\": {\n\t\t\"defaultMessage\": \"301 Kalıcı olarak taşındı\"\n\t},\n\t\"redirection-hosts.http-code.302\": {\n\t\t\"defaultMessage\": \"302 Geçici olarak taşındı\"\n\t},\n\t\"redirection-hosts.http-code.303\": {\n\t\t\"defaultMessage\": \"303 Diğerini gör\"\n\t},\n\t\"redirection-hosts.http-code.307\": {\n\t\t\"defaultMessage\": \"307 Geçici yönlendirme\"\n\t},\n\t\"redirection-hosts.http-code.308\": {\n\t\t\"defaultMessage\": \"308 Kalıcı yönlendirme\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Yönetici\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Standart Kullanıcı\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Kaydet\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Ayar\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Ayarlar\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Varsayılan Site\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"404 Sayfası\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Yanıt Yok (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Tebrikler Sayfası\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Nginx bilinmeyen bir Host ile karşılaştığında ne gösterilecek\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"Özel HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Özel HTML içeriğinizi buraya girin -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Yönlendir\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Yönetici hesabınızı oluşturarak başlayın.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Hoş Geldiniz!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Giriş yap\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL Sertifikası\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Akış\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"İletme Host'u\"\n\t},\n\t\"stream.forward-host.placeholder\": {\n\t\t\"defaultMessage\": \"example.com veya 10.0.0.1 veya 2001:db8:3333:4444:5555:6666:7777:8888\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Gelen Port\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Akışlar\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} {count, plural, one {Akış} other {Akış}}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Test\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Güncelleme Mevcut: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Kullanıcı\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Şifreyi Değiştir\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Şifreyi Onayla\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Mevcut Şifre\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Profili Düzenle\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Ad Soyad\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"{name} olarak giriş yap\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Çıkış Yap\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Yeni Şifre\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Takma Ad\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Şifre Belirle\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"{name} için İzinleri Belirle\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Karanlık moda geç\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Açık moda geç\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Kullanıcı Adı\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Kullanıcılar\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/vi.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"Danh sách truy cập\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} quy tắc\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} người dùng\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"Quy tắc từ chối tất cả này sẽ được thêm vào cuối khi tồn tại ít nhất 1 quy tắc\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \"Các quy tắc cho phép và từ chối sẽ được thực thi theo thứ tự được xác định.\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"Chuyển xác thực lên thượng nguồn\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"Có thể truy cập công khai\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"Không cần xác thực cơ bản\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"Thỏa mãn điều kiện bất kỳ\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} người dùng, {rules} quy tắc - Tạo lúc: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"Danh sách truy cập\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"Thêm\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"Thêm Vị trí\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"Đóng\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"Xóa\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"Tắt\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"Tải xuống\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"Chỉnh sửa\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"Bật\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"Quyền\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"Gia hạn\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"Xem Chi tiết\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"Nhật ký kiểm tra\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"Hủy\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"Chứng chỉ\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"Certificate (crt)\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"Certificate Key\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"Intermediate Certificate\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"Đang sử dụng\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"Không có chứng chỉ nào được chỉ định\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"Máy chủ này sẽ không sử dụng HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"Không có\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"Không được dùng\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"Gia hạn Chứng chỉ\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"Danh sách chứng chỉ\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"Chứng chỉ tùy chỉnh\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"Các tệp chính được bảo vệ bằng cụm mật khẩu không được hỗ trợ.\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"Nội dung tệp thông tin xác thực\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"Plugin này yêu cầu tệp cấu hình chứa mã thông báo API hoặc thông tin xác thực khác cho nhà cung cấp của bạn\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"Dữ liệu này sẽ được lưu trữ dưới dạng bản rõ trong cơ sở dữ liệu và trong một tệp!\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"Thời gian lan truyền (Giây)\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"Để trống để sử dụng giá trị mặc định của plugin. Số giây chờ truyền DNS.\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"Nhà cung cấp DNS\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"Phần này yêu cầu một số kiến thức về Certbot và các plugin DNS của nó. Vui lòng tham khảo tài liệu plugin tương ứng.\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"Có một máy chủ được tìm thấy ở miền này nhưng có vẻ như nó không phải là NPM. Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy.\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"Không thể kiểm tra khả năng truy cập do lỗi giao tiếp với site24x7.com.\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"Không có máy chủ có sẵn tại tên miền này. Vui lòng đảm bảo rằng miền của bạn tồn tại và trỏ đến IP nơi phiên bản NPM của bạn đang chạy và nếu cần, cổng 80 sẽ được chuyển tiếp trong bộ định tuyến của bạn.\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"Máy chủ của bạn có thể truy cập được và có thể tạo chứng chỉ.\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"Có một máy chủ được tìm thấy ở miền này nhưng nó trả về mã trạng thái không mong muốn {code}. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy.\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"Có một máy chủ được tìm thấy ở miền này nhưng nó trả về một dữ liệu không mong muốn. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy.\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"Kết quả kiểm tra\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"Các miền này phải được cấu hình sẵn để trỏ đến cài đặt này.\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"Loại khóa\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA tương thích rộng rãi, ECDSA nhanh hơn và an toàn hơn nhưng có thể không được hỗ trợ bởi các hệ thống cũ\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"bằng Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"Yêu cầu chứng chỉ mới\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"Truy cập\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"Ủy quyền\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"Danh sách ủy quyền\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"Quy tắc đường dẫn tùy chỉnh (Vị trí)\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"Mục tiêu\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"Chi tiết\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"Email\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"Sự kiện\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"Hết hạn\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"Cổng đến\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"Tên\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"Giao thức\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"Nhà cung cấp\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"Vai trò\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"Quy tắc\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"Thỏa mãn\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"Tất cả\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"Bất kì\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"Nguồn\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"Trạng thái\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"Đã tạo: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"Bảng điều khiển\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"Máy chủ 404\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"Máy chủ 404\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"Số trang lỗi {count}\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"Đã tắt\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"Danh sách tên miền\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"Tối đa {count} tên miền\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"Nhập tên miền vào đây...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"Ký tự đại diện không được phép cho loại này\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"Ký tự đại diện không được hỗ trợ cho CA này\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"Bắt buộc SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"Bật HSTS\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"Tên miền phụ HSTS\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"Hỗ trợ HTTP/2\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"Dùng thử thách DNS\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"Địa chỉ email\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"Không có kết quả nào\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"Tại sao bạn không tạo một cái luôn?\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"Đã bật\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"Yêu cầu ít nhất một quy tắc ủy quyền hoặc truy cập\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"Tên người dùng được ủy quyền phải là duy nhất\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"Email hoặc Mật khẩu không hợp lệ\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"Tên miền không hợp lệ: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"Địa chỉ email không hợp lệ\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"Độ dài tối đa là {max} ký tự\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"Quá nhiều tên miền, tối đa là {max}\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"Tối đa là {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"Độ dài tối thiểu là {min} ký tự\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"Tối thiểu là {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"Mật khẩu phải khớp\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"Điều này là bắt buộc\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"Hết hạn: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"Fork dự án này trên Github\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"Chặn các hoạt động khai thác phổ biến\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"Cache tài nguyên\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"Bảo toàn đường dẫn\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"Giao thức\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Hỗ trợ Websockets\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"Chuyển tiếp cổng\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"Scheme\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"Máy chủ\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"HTTP Only\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt qua DNS\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt qua HTTP\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"Đang tải...\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"Đăng nhập vào tài khoản của bạn\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"Cấu hình Nginx tùy chỉnh\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# Nhập cấu hình Nginx tùy chỉnh của bạn vào đây và bạn phải tự chịu rủi ro!\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"Bạn không có quyền truy cập trang này.\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"Về trang chủ\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"Chúng tôi xin lỗi nhưng trang bạn đang tìm kiếm không được tìm thấy\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"Rất tiếc… Bạn vừa tìm thấy một trang lỗi\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"Lỗi\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} đã được xóa\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} đã được tắt\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} đã được bật\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} đã được làm mới\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} đã được lưu\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"Thành công\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"Thêm {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"Xóa {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"Bạn có chắc muốn xóa {object} không?\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"Chỉnh sửa {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"Không có {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"Đã tạo {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"Đã xóa {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"Đã tắt {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"Đã bật {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"Đã gia hạn {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"Đã cập nhật {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"Ngoại tuyến\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"Trực tuyến\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"Tùy chọn\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"Mật khẩu\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"Tạo mật khẩu ngẫu nhiên\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"Ẩn Mật khẩu\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"Hiện Mật khẩu\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"Ẩn\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"Quản lý\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"Chỉ xem\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"Tất cả các mục\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"Khả năng hiển thị mục\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"Chỉ các mục đã tạo\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"Máy chủ proxy\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"Chuyển tiếp Hostname / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"Máy chủ proxy\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} máy chủ proxy\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"Công khai\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"Redirection Host\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"Chuyển tiếp Tên miền\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP Code\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"Redirection Hosts\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} máy chủ chuyển hướng\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"Quản trị viên\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"Người dùng bình thường\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"Lưu\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"Cài đặt\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"Cài đặt\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"Trang web mặc định\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"Trang 404\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"Không có phản hồi (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"Trang chào mừng\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"Hiển thị gì khi Nginx gặp phải Máy chủ không xác định\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"HTML tùy chỉnh\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- Nhập nội dung HTML tùy chỉnh tại đây -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"Chuyển hướng\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"Bắt đầu bằng cách tạo tài khoản quản trị viên.\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"Chào mừng!\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"Đăng nhập\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"Chứng chỉ SSL\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"Stream\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"Chuyển tiếp Host\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"Cổng vào\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"Danh sách các Stream\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"Số Stream {count}\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"Kiểm tra\"\n\t},\n\t\"update-available\": {\n\t\t\"defaultMessage\": \"Cập nhật khả dụng: {latestVersion}\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"Người dùng\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"Đổi Mật khẩu\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"Xác nhận Mật khẩu\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"Mật khẩu hiện tại\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"Chỉnh sửa hồ sơ\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"Tên\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"Đăng nhập bằng {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"Đăng xuất\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"Mật khẩu mới\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"Tên hiển thị\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"Đặt Mật khẩu\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"Đặt quyền cho {name}\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"Chuyển sang chế độ tối\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"Chuyển sang chế độ sáng\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"Tên người dùng\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"Danh sách người dùng\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/locale/src/zh.json",
    "content": "{\n\t\"access-list\": {\n\t\t\"defaultMessage\": \"通信规则\"\n\t},\n\t\"access-list.access-count\": {\n\t\t\"defaultMessage\": \"{count} 条规则\"\n\t},\n\t\"access-list.auth-count\": {\n\t\t\"defaultMessage\": \"{count} 个用户\"\n\t},\n\t\"access-list.help-rules-last\": {\n\t\t\"defaultMessage\": \"当至少存在1条规则时，此拒绝所有规则将被添加到最后\"\n\t},\n\t\"access-list.help.rules-order\": {\n\t\t\"defaultMessage\": \" 允许 (allow) 和禁止 (deny) 规则将按照它们定义的顺序执行。\"\n\t},\n\t\"access-list.pass-auth\": {\n\t\t\"defaultMessage\": \"将认证传递给上游\"\n\t},\n\t\"access-list.public\": {\n\t\t\"defaultMessage\": \"公开可访问\"\n\t},\n\t\"access-list.public.subtitle\": {\n\t\t\"defaultMessage\": \"无需基本认证\"\n\t},\n\t\"access-list.satisfy-any\": {\n\t\t\"defaultMessage\": \"满足任意条件\"\n\t},\n\t\"access-list.subtitle\": {\n\t\t\"defaultMessage\": \"{users} 个用户, {rules} 条规则 - 创建时间: {date}\"\n\t},\n\t\"access-lists\": {\n\t\t\"defaultMessage\": \"通信规则\"\n\t},\n\t\"action.add\": {\n\t\t\"defaultMessage\": \"添加\"\n\t},\n\t\"action.add-location\": {\n\t\t\"defaultMessage\": \"添加路径规则(Location)\"\n\t},\n\t\"action.close\": {\n\t\t\"defaultMessage\": \"关闭\"\n\t},\n\t\"action.delete\": {\n\t\t\"defaultMessage\": \"删除\"\n\t},\n\t\"action.disable\": {\n\t\t\"defaultMessage\": \"禁用\"\n\t},\n\t\"action.download\": {\n\t\t\"defaultMessage\": \"下载\"\n\t},\n\t\"action.edit\": {\n\t\t\"defaultMessage\": \"编辑\"\n\t},\n\t\"action.enable\": {\n\t\t\"defaultMessage\": \"启用\"\n\t},\n\t\"action.permissions\": {\n\t\t\"defaultMessage\": \"权限\"\n\t},\n\t\"action.renew\": {\n\t\t\"defaultMessage\": \"续期\"\n\t},\n\t\"action.view-details\": {\n\t\t\"defaultMessage\": \"查看详情\"\n\t},\n\t\"auditlogs\": {\n\t\t\"defaultMessage\": \"审计日志\"\n\t},\n\t\"cancel\": {\n\t\t\"defaultMessage\": \"取消\"\n\t},\n\t\"certificate\": {\n\t\t\"defaultMessage\": \"证书\"\n\t},\n\t\"certificate.custom-certificate\": {\n\t\t\"defaultMessage\": \"证书\"\n\t},\n\t\"certificate.custom-certificate-key\": {\n\t\t\"defaultMessage\": \"证书密钥\"\n\t},\n\t\"certificate.custom-intermediate\": {\n\t\t\"defaultMessage\": \"中间证书\"\n\t},\n\t\"certificate.in-use\": {\n\t\t\"defaultMessage\": \"使用中\"\n\t},\n\t\"certificate.none.subtitle\": {\n\t\t\"defaultMessage\": \"未分配证书\"\n\t},\n\t\"certificate.none.subtitle.for-http\": {\n\t\t\"defaultMessage\": \"此主机将不使用 HTTPS\"\n\t},\n\t\"certificate.none.title\": {\n\t\t\"defaultMessage\": \"无\"\n\t},\n\t\"certificate.not-in-use\": {\n\t\t\"defaultMessage\": \"未使用\"\n\t},\n\t\"certificate.renew\": {\n\t\t\"defaultMessage\": \"续期证书\"\n\t},\n\t\"certificates\": {\n\t\t\"defaultMessage\": \"证书列表\"\n\t},\n\t\"certificates.custom\": {\n\t\t\"defaultMessage\": \"自定义证书\"\n\t},\n\t\"certificates.custom.warning\": {\n\t\t\"defaultMessage\": \"不支持受密码保护的密钥文件。\"\n\t},\n\t\"certificates.dns.credentials\": {\n\t\t\"defaultMessage\": \"凭据文件内容\"\n\t},\n\t\"certificates.dns.credentials-note\": {\n\t\t\"defaultMessage\": \"此插件需要一个包含 API 令牌或提供商其他凭证的配置文件\"\n\t},\n\t\"certificates.dns.credentials-warning\": {\n\t\t\"defaultMessage\": \"此数据将以明文形式存储在数据库和文件中！\"\n\t},\n\t\"certificates.dns.propagation-seconds\": {\n\t\t\"defaultMessage\": \"传播时间 (秒)\"\n\t},\n\t\"certificates.dns.propagation-seconds-note\": {\n\t\t\"defaultMessage\": \"留空以使用插件默认值。等待DNS传播的秒数。\"\n\t},\n\t\"certificates.dns.provider\": {\n\t\t\"defaultMessage\": \"DNS 提供商\"\n\t},\n\t\"certificates.dns.warning\": {\n\t\t\"defaultMessage\": \"本节需要您具备一些关于 Certbot 及其 DNS 插件的知识，请参阅相应插件的官方文档。\"\n\t},\n\t\"certificates.http.reachability-404\": {\n\t\t\"defaultMessage\": \"在此域名下找到了一个服务器，但它似乎不是 Nginx 代理管理器。请确保您的域名指向 NPM 实例运行的 IP 地址。\"\n\t},\n\t\"certificates.http.reachability-failed-to-check\": {\n\t\t\"defaultMessage\": \"由于与site24x7.com通信错误，无法检查可达性。\"\n\t},\n\t\"certificates.http.reachability-not-resolved\": {\n\t\t\"defaultMessage\": \"此域名下没有可用的服务器。请确保您的域名存在并指向NPM实例运行的 IP 地址，如有必要，请在路由器中转发 80 端口。\"\n\t},\n\t\"certificates.http.reachability-ok\": {\n\t\t\"defaultMessage\": \"您的服务器可以访问，应该可以创建证书。\"\n\t},\n\t\"certificates.http.reachability-other\": {\n\t\t\"defaultMessage\": \"在此域名下找到了一个服务器，但它返回了意外的状态码 {code}。它是 NPM 服务器吗？请确保您的域名指向NPM实例运行的 IP 地址。\"\n\t},\n\t\"certificates.http.reachability-wrong-data\": {\n\t\t\"defaultMessage\": \"在此域名下找到了一个服务器，但它返回了意外的数据。它是 NPM 服务器吗？请确保您的域名指向 NPM 实例运行的 IP 地址。\"\n\t},\n\t\"certificates.http.test-results\": {\n\t\t\"defaultMessage\": \"测试结果\"\n\t},\n\t\"certificates.http.warning\": {\n\t\t\"defaultMessage\": \"这些域名必须配置为指向本设备。\"\n\t},\n\t\"certificates.key-type\": {\n\t\t\"defaultMessage\": \"密钥类型\"\n\t},\n\t\"certificates.key-type-description\": {\n\t\t\"defaultMessage\": \"RSA 兼容性更好，ECDSA 更快更安全但旧系统可能不支持\"\n\t},\n\t\"certificates.key-type-ecdsa\": {\n\t\t\"defaultMessage\": \"ECDSA 256\"\n\t},\n\t\"certificates.key-type-rsa\": {\n\t\t\"defaultMessage\": \"RSA 2048\"\n\t},\n\t\"certificates.request.subtitle\": {\n\t\t\"defaultMessage\": \"使用 Let's Encrypt\"\n\t},\n\t\"certificates.request.title\": {\n\t\t\"defaultMessage\": \"申请新证书\"\n\t},\n\t\"column.access\": {\n\t\t\"defaultMessage\": \"访问\"\n\t},\n\t\"column.authorization\": {\n\t\t\"defaultMessage\": \"授权\"\n\t},\n\t\"column.authorizations\": {\n\t\t\"defaultMessage\": \"授权列表\"\n\t},\n\t\"column.custom-locations\": {\n\t\t\"defaultMessage\": \"自定义路径规则 (Locations)\"\n\t},\n\t\"column.destination\": {\n\t\t\"defaultMessage\": \"目标\"\n\t},\n\t\"column.details\": {\n\t\t\"defaultMessage\": \"详情\"\n\t},\n\t\"column.email\": {\n\t\t\"defaultMessage\": \"邮箱\"\n\t},\n\t\"column.event\": {\n\t\t\"defaultMessage\": \"事件\"\n\t},\n\t\"column.expires\": {\n\t\t\"defaultMessage\": \"过期时间\"\n\t},\n\t\"column.http-code\": {\n\t\t\"defaultMessage\": \"访问\"\n\t},\n\t\"column.incoming-port\": {\n\t\t\"defaultMessage\": \"入站端口\"\n\t},\n\t\"column.name\": {\n\t\t\"defaultMessage\": \"名称\"\n\t},\n\t\"column.protocol\": {\n\t\t\"defaultMessage\": \"协议\"\n\t},\n\t\"column.provider\": {\n\t\t\"defaultMessage\": \"提供商\"\n\t},\n\t\"column.roles\": {\n\t\t\"defaultMessage\": \"角色\"\n\t},\n\t\"column.rules\": {\n\t\t\"defaultMessage\": \"规则\"\n\t},\n\t\"column.satisfy\": {\n\t\t\"defaultMessage\": \"满足\"\n\t},\n\t\"column.satisfy-all\": {\n\t\t\"defaultMessage\": \"全部\"\n\t},\n\t\"column.satisfy-any\": {\n\t\t\"defaultMessage\": \"任意\"\n\t},\n\t\"column.scheme\": {\n\t\t\"defaultMessage\": \"协议\"\n\t},\n\t\"column.source\": {\n\t\t\"defaultMessage\": \"来源\"\n\t},\n\t\"column.ssl\": {\n\t\t\"defaultMessage\": \"SSL\"\n\t},\n\t\"column.status\": {\n\t\t\"defaultMessage\": \"状态\"\n\t},\n\t\"created-on\": {\n\t\t\"defaultMessage\": \"创建时间: {date}\"\n\t},\n\t\"dashboard\": {\n\t\t\"defaultMessage\": \"仪表板\"\n\t},\n\t\"dead-host\": {\n\t\t\"defaultMessage\": \"错误页面\"\n\t},\n\t\"dead-hosts\": {\n\t\t\"defaultMessage\": \"错误页面列表\"\n\t},\n\t\"dead-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} 个错误页面列表\"\n\t},\n\t\"disabled\": {\n\t\t\"defaultMessage\": \"已禁用\"\n\t},\n\t\"domain-names\": {\n\t\t\"defaultMessage\": \"域名\"\n\t},\n\t\"domain-names.max\": {\n\t\t\"defaultMessage\": \"{count} 个最多域名数量\"\n\t},\n\t\"domain-names.placeholder\": {\n\t\t\"defaultMessage\": \"开始输入以添加域名...\"\n\t},\n\t\"domain-names.wildcards-not-permitted\": {\n\t\t\"defaultMessage\": \"此类型不允许使用通配符\"\n\t},\n\t\"domain-names.wildcards-not-supported\": {\n\t\t\"defaultMessage\": \"此 CA 不支持通配符\"\n\t},\n\t\"domains.advanced\": {\n\t\t\"defaultMessage\": \"高级选项\"\n\t},\n\t\"domains.force-ssl\": {\n\t\t\"defaultMessage\": \"强制 SSL\"\n\t},\n\t\"domains.hsts-enabled\": {\n\t\t\"defaultMessage\": \"HSTS 已启用\"\n\t},\n\t\"domains.hsts-subdomains\": {\n\t\t\"defaultMessage\": \"HSTS 子域名\"\n\t},\n\t\"domains.http2-support\": {\n\t\t\"defaultMessage\": \"HTTP/2 支持\"\n\t},\n\t\"domains.trust-forwarded-proto\": {\n\t\t\"defaultMessage\": \"信任上游代理传递的协议类型头\"\n\t},\n\t\"domains.use-dns\": {\n\t\t\"defaultMessage\": \"使用DNS验证\"\n\t},\n\t\"email-address\": {\n\t\t\"defaultMessage\": \"邮箱地址\"\n\t},\n\t\"empty-search\": {\n\t\t\"defaultMessage\": \"未找到结果\"\n\t},\n\t\"empty-subtitle\": {\n\t\t\"defaultMessage\": \"为什么不由您来创建一个呢？\"\n\t},\n\t\"enabled\": {\n\t\t\"defaultMessage\": \"已启用\"\n\t},\n\t\"error.access.at-least-one\": {\n\t\t\"defaultMessage\": \"需要至少一个授权或访问规则\"\n\t},\n\t\"error.access.duplicate-usernames\": {\n\t\t\"defaultMessage\": \"授权用户名必须唯一\"\n\t},\n\t\"error.invalid-auth\": {\n\t\t\"defaultMessage\": \"无效的邮箱或密码\"\n\t},\n\t\"error.invalid-domain\": {\n\t\t\"defaultMessage\": \"无效的域名: {domain}\"\n\t},\n\t\"error.invalid-email\": {\n\t\t\"defaultMessage\": \"无效的邮箱地址\"\n\t},\n\t\"error.max-character-length\": {\n\t\t\"defaultMessage\": \"最大长度为 {max} 个字符\"\n\t},\n\t\"error.max-domains\": {\n\t\t\"defaultMessage\": \"域名过多，最多为 {max} 个\"\n\t},\n\t\"error.maximum\": {\n\t\t\"defaultMessage\": \"最大值为 {max}\"\n\t},\n\t\"error.min-character-length\": {\n\t\t\"defaultMessage\": \"最小长度为 {min} 个字符\"\n\t},\n\t\"error.minimum\": {\n\t\t\"defaultMessage\": \"最小值为 {min}\"\n\t},\n\t\"error.passwords-must-match\": {\n\t\t\"defaultMessage\": \"密码必须匹配\"\n\t},\n\t\"error.required\": {\n\t\t\"defaultMessage\": \"此项为必填项\"\n\t},\n\t\"expires.on\": {\n\t\t\"defaultMessage\": \"过期时间: {date}\"\n\t},\n\t\"footer.github-fork\": {\n\t\t\"defaultMessage\": \"在 Github 上复刻 (Fork) 本项目\"\n\t},\n\t\"host.flags.block-exploits\": {\n\t\t\"defaultMessage\": \"阻止常见攻击\"\n\t},\n\t\"host.flags.cache-assets\": {\n\t\t\"defaultMessage\": \"缓存资源\"\n\t},\n\t\"host.flags.preserve-path\": {\n\t\t\"defaultMessage\": \"保留路径\"\n\t},\n\t\"host.flags.protocols\": {\n\t\t\"defaultMessage\": \"协议\"\n\t},\n\t\"host.flags.websockets-upgrade\": {\n\t\t\"defaultMessage\": \"Websockets 支持\"\n\t},\n\t\"host.forward-port\": {\n\t\t\"defaultMessage\": \"转发端口\"\n\t},\n\t\"host.forward-scheme\": {\n\t\t\"defaultMessage\": \"协议\"\n\t},\n\t\"hosts\": {\n\t\t\"defaultMessage\": \"主机列表\"\n\t},\n\t\"http-only\": {\n\t\t\"defaultMessage\": \"仅 HTTP\"\n\t},\n\t\"lets-encrypt\": {\n\t\t\"defaultMessage\": \"Let's Encrypt\"\n\t},\n\t\"lets-encrypt-via-dns\": {\n\t\t\"defaultMessage\": \"Let's Encrypt DNS 验证\"\n\t},\n\t\"lets-encrypt-via-http\": {\n\t\t\"defaultMessage\": \"Let's Encrypt HTTP 验证\"\n\t},\n\t\"loading\": {\n\t\t\"defaultMessage\": \"加载中···\"\n\t},\n\t\"login.title\": {\n\t\t\"defaultMessage\": \"登录您的账户\"\n\t},\n\t\"nginx-config.label\": {\n\t\t\"defaultMessage\": \"自定义 Nginx 配置\"\n\t},\n\t\"nginx-config.placeholder\": {\n\t\t\"defaultMessage\": \"# 在此输入您的自定义 Nginx 配置，风险自负！\"\n\t},\n\t\"no-permission-error\": {\n\t\t\"defaultMessage\": \"您无权查看此内容。\"\n\t},\n\t\"notfound.action\": {\n\t\t\"defaultMessage\": \"返回首页\"\n\t},\n\t\"notfound.content\": {\n\t\t\"defaultMessage\": \"很抱歉，您要查找的页面未找到\"\n\t},\n\t\"notfound.title\": {\n\t\t\"defaultMessage\": \"糟糕...您刚刚找到了一个错误页面\"\n\t},\n\t\"notification.error\": {\n\t\t\"defaultMessage\": \"错误\"\n\t},\n\t\"notification.object-deleted\": {\n\t\t\"defaultMessage\": \"{object} 已被删除\"\n\t},\n\t\"notification.object-disabled\": {\n\t\t\"defaultMessage\": \"{object} 已被禁用\"\n\t},\n\t\"notification.object-enabled\": {\n\t\t\"defaultMessage\": \"{object} 已被启用\"\n\t},\n\t\"notification.object-renewed\": {\n\t\t\"defaultMessage\": \"{object} 已续期\"\n\t},\n\t\"notification.object-saved\": {\n\t\t\"defaultMessage\": \"{object} 已保存\"\n\t},\n\t\"notification.success\": {\n\t\t\"defaultMessage\": \"成功\"\n\t},\n\t\"object.actions-title\": {\n\t\t\"defaultMessage\": \"{object} #{id}\"\n\t},\n\t\"object.add\": {\n\t\t\"defaultMessage\": \"添加 {object}\"\n\t},\n\t\"object.delete\": {\n\t\t\"defaultMessage\": \"删除 {object}\"\n\t},\n\t\"object.delete.content\": {\n\t\t\"defaultMessage\": \"您确定要删除 {object} 吗？\"\n\t},\n\t\"object.edit\": {\n\t\t\"defaultMessage\": \"编辑 {object}\"\n\t},\n\t\"object.empty\": {\n\t\t\"defaultMessage\": \"没有 {objects}\"\n\t},\n\t\"object.event.created\": {\n\t\t\"defaultMessage\": \"已创建 {object}\"\n\t},\n\t\"object.event.deleted\": {\n\t\t\"defaultMessage\": \"已删除 {object}\"\n\t},\n\t\"object.event.disabled\": {\n\t\t\"defaultMessage\": \"已禁用 {object}\"\n\t},\n\t\"object.event.enabled\": {\n\t\t\"defaultMessage\": \"已启用 {object}\"\n\t},\n\t\"object.event.renewed\": {\n\t\t\"defaultMessage\": \"已续期 {object}\"\n\t},\n\t\"object.event.updated\": {\n\t\t\"defaultMessage\": \"已更新 {object}\"\n\t},\n\t\"offline\": {\n\t\t\"defaultMessage\": \"离线\"\n\t},\n\t\"online\": {\n\t\t\"defaultMessage\": \"在线\"\n\t},\n\t\"options\": {\n\t\t\"defaultMessage\": \"选项\"\n\t},\n\t\"password\": {\n\t\t\"defaultMessage\": \"密码\"\n\t},\n\t\"password.generate\": {\n\t\t\"defaultMessage\": \"生成随机密码\"\n\t},\n\t\"password.hide\": {\n\t\t\"defaultMessage\": \"隐藏密码\"\n\t},\n\t\"password.show\": {\n\t\t\"defaultMessage\": \"显示密码\"\n\t},\n\t\"permissions.hidden\": {\n\t\t\"defaultMessage\": \"隐藏\"\n\t},\n\t\"permissions.manage\": {\n\t\t\"defaultMessage\": \"管理\"\n\t},\n\t\"permissions.view\": {\n\t\t\"defaultMessage\": \"仅查看\"\n\t},\n\t\"permissions.visibility.all\": {\n\t\t\"defaultMessage\": \"所有项目\"\n\t},\n\t\"permissions.visibility.title\": {\n\t\t\"defaultMessage\": \"项目可见性\"\n\t},\n\t\"permissions.visibility.user\": {\n\t\t\"defaultMessage\": \"仅创建的项目\"\n\t},\n\t\"proxy-host\": {\n\t\t\"defaultMessage\": \"代理服务\"\n\t},\n\t\"proxy-host.forward-host\": {\n\t\t\"defaultMessage\": \"转发主机名 / IP\"\n\t},\n\t\"proxy-hosts\": {\n\t\t\"defaultMessage\": \"代理服务列表\"\n\t},\n\t\"proxy-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} 个代理服务\"\n\t},\n\t\"public\": {\n\t\t\"defaultMessage\": \"公开\"\n\t},\n\t\"redirection-host\": {\n\t\t\"defaultMessage\": \"重定向主机\"\n\t},\n\t\"redirection-host.forward-domain\": {\n\t\t\"defaultMessage\": \"转发域名\"\n\t},\n\t\"redirection-host.forward-http-code\": {\n\t\t\"defaultMessage\": \"HTTP 状态码\"\n\t},\n\t\"redirection-hosts\": {\n\t\t\"defaultMessage\": \"重定向主机列表\"\n\t},\n\t\"redirection-hosts.count\": {\n\t\t\"defaultMessage\": \"{count} 个重定向主机\"\n\t},\n\t\"role.admin\": {\n\t\t\"defaultMessage\": \"管理员\"\n\t},\n\t\"role.standard-user\": {\n\t\t\"defaultMessage\": \"标准用户\"\n\t},\n\t\"save\": {\n\t\t\"defaultMessage\": \"保存\"\n\t},\n\t\"setting\": {\n\t\t\"defaultMessage\": \"设置\"\n\t},\n\t\"settings\": {\n\t\t\"defaultMessage\": \"设置列表\"\n\t},\n\t\"settings.default-site\": {\n\t\t\"defaultMessage\": \"默认站点\"\n\t},\n\t\"settings.default-site.404\": {\n\t\t\"defaultMessage\": \"错误页面\"\n\t},\n\t\"settings.default-site.444\": {\n\t\t\"defaultMessage\": \"无响应 (444)\"\n\t},\n\t\"settings.default-site.congratulations\": {\n\t\t\"defaultMessage\": \"欢迎页面\"\n\t},\n\t\"settings.default-site.description\": {\n\t\t\"defaultMessage\": \"当 Nginx 遇到未知主机时显示什么\"\n\t},\n\t\"settings.default-site.html\": {\n\t\t\"defaultMessage\": \"自定义 HTML\"\n\t},\n\t\"settings.default-site.html.placeholder\": {\n\t\t\"defaultMessage\": \"<!-- 在此输入您的自定义 HTML 内容 -->\"\n\t},\n\t\"settings.default-site.redirect\": {\n\t\t\"defaultMessage\": \"重定向\"\n\t},\n\t\"setup.preamble\": {\n\t\t\"defaultMessage\": \"通过创建您的管理员账户开始使用。\"\n\t},\n\t\"setup.title\": {\n\t\t\"defaultMessage\": \"欢迎！\"\n\t},\n\t\"sign-in\": {\n\t\t\"defaultMessage\": \"登录\"\n\t},\n\t\"ssl-certificate\": {\n\t\t\"defaultMessage\": \"SSL 证书\"\n\t},\n\t\"stream\": {\n\t\t\"defaultMessage\": \"端口转发\"\n\t},\n\t\"stream.forward-host\": {\n\t\t\"defaultMessage\": \"转发主机\"\n\t},\n\t\"stream.incoming-port\": {\n\t\t\"defaultMessage\": \"入站端口\"\n\t},\n\t\"streams\": {\n\t\t\"defaultMessage\": \"端口转发列表\"\n\t},\n\t\"streams.count\": {\n\t\t\"defaultMessage\": \"{count} 个端口转发\"\n\t},\n\t\"streams.tcp\": {\n\t\t\"defaultMessage\": \"TCP\"\n\t},\n\t\"streams.udp\": {\n\t\t\"defaultMessage\": \"UDP\"\n\t},\n\t\"test\": {\n\t\t\"defaultMessage\": \"测试\"\n\t},\n\t\"user\": {\n\t\t\"defaultMessage\": \"用户\"\n\t},\n\t\"user.change-password\": {\n\t\t\"defaultMessage\": \"修改密码\"\n\t},\n\t\"user.confirm-password\": {\n\t\t\"defaultMessage\": \"确认密码\"\n\t},\n\t\"user.current-password\": {\n\t\t\"defaultMessage\": \"当前密码\"\n\t},\n\t\"user.edit-profile\": {\n\t\t\"defaultMessage\": \"编辑资料\"\n\t},\n\t\"user.full-name\": {\n\t\t\"defaultMessage\": \"全名\"\n\t},\n\t\"user.login-as\": {\n\t\t\"defaultMessage\": \"登录用户 {name}\"\n\t},\n\t\"user.logout\": {\n\t\t\"defaultMessage\": \"退出登录\"\n\t},\n\t\"user.new-password\": {\n\t\t\"defaultMessage\": \"新密码\"\n\t},\n\t\"user.nickname\": {\n\t\t\"defaultMessage\": \"昵称\"\n\t},\n\t\"user.set-password\": {\n\t\t\"defaultMessage\": \"设置密码\"\n\t},\n\t\"user.set-permissions\": {\n\t\t\"defaultMessage\": \"为用户 {name} 设置权限\"\n\t},\n\t\"user.switch-dark\": {\n\t\t\"defaultMessage\": \"切换到深色模式\"\n\t},\n\t\"user.switch-light\": {\n\t\t\"defaultMessage\": \"切换到浅色模式\"\n\t},\n\t\"username\": {\n\t\t\"defaultMessage\": \"用户名\"\n\t},\n\t\"users\": {\n\t\t\"defaultMessage\": \"用户列表\"\n\t}\n}\n"
  },
  {
    "path": "frontend/src/main.tsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"src/App.tsx\";\n\nimport \"@tabler/core/dist/css/tabler.min.css\";\nimport \"@tabler/core/dist/js/tabler.min.js\";\nimport \"./App.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\") as HTMLElement).render(\n\t<React.StrictMode>\n\t\t<App />\n\t</React.StrictMode>,\n);\n"
  },
  {
    "path": "frontend/src/modals/AccessListModal.tsx",
    "content": "import cn from \"classnames\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport type { AccessList, AccessListClient, AccessListItem } from \"src/api/backend\";\nimport { AccessClientFields, BasicAuthFields, Button, Loading } from \"src/components\";\nimport { useAccessList, useSetAccessList } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showAccessListModal = (id: number | \"new\") => {\n\tEasyModal.show(AccessListModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"new\";\n}\nconst AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useAccessList(id, [\"items\", \"clients\"]);\n\tconst { mutate: setAccessList } = useSetAccessList();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst validate = (values: any): string | null => {\n\t\t// either Auths or Clients must be defined\n\t\tif (values.items?.length === 0 && values.clients?.length === 0) {\n\t\t\treturn intl.formatMessage({ id: \"error.access.at-least-one\" });\n\t\t}\n\n\t\t// ensure the items don't contain the same username twice\n\t\tconst usernames = values.items.map((i: any) => i.username);\n\t\tconst uniqueUsernames = Array.from(new Set(usernames));\n\t\tif (usernames.length !== uniqueUsernames.length) {\n\t\t\treturn intl.formatMessage({ id: \"error.access.duplicate-usernames\" });\n\t\t}\n\n\t\treturn null;\n\t};\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\n\t\tconst vErr = validate(values);\n\t\tif (vErr) {\n\t\t\tsetErrorMsg(vErr);\n\t\t\treturn;\n\t\t}\n\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\t...values,\n\t\t};\n\n\t\t// Filter out \"items\" to only use the \"username\" and \"password\" fields\n\t\tpayload.items = (values.items || []).map((i: AccessListItem) => ({\n\t\t\tusername: i.username,\n\t\t\tpassword: i.password,\n\t\t}));\n\n\t\t// Filter out \"clients\" to only use the \"directive\" and \"address\" fields\n\t\tpayload.clients = (values.clients || []).map((i: AccessListClient) => ({\n\t\t\tdirective: i.directive,\n\t\t\taddress: i.address,\n\t\t}));\n\n\t\tsetAccessList(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"access-list\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\tconst toggleClasses = \"form-check-input\";\n\tconst toggleEnabled = cn(toggleClasses, \"bg-cyan\");\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: data?.name,\n\t\t\t\t\t\t\tsatisfyAny: data?.satisfyAny,\n\t\t\t\t\t\t\tpassAuth: data?.passAuth,\n\t\t\t\t\t\t\titems: data?.items || [],\n\t\t\t\t\t\t\tclients: data?.clients || [],\n\t\t\t\t\t\t} as AccessList\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{({ setFieldValue }: any) => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id={data?.id ? \"object.edit\" : \"object.add\"} tData={{ object: \"access-list\" }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t\t\t\t\t\t<ul className=\"nav nav-tabs card-header-tabs\" data-bs-toggle=\"tabs\">\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-details\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link active\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.details\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-auth\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.authorizations\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-rules\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.rules\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"tab-content\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane active show\" id=\"tab-details\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"name\" validate={validateString(1, 255)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"name\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"name\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name && form.touched.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"my-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"options\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"divide-y\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"satisfyAny\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"access-list.satisfy-any\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"satisfyAny\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"satisfyAny\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.value\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? toggleEnabled\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: toggleClasses\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.target.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"passAuth\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"access-list.pass-auth\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"passAuth\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"passAuth\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.value\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? toggleEnabled\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: toggleClasses\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.target.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-auth\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<BasicAuthFields initialValues={data?.items || []} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-rules\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<AccessClientFields initialValues={data?.clients || []} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-cyan\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showAccessListModal };\n"
  },
  {
    "path": "frontend/src/modals/ChangePasswordModal.tsx",
    "content": "import EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { updateAuth } from \"src/api/backend\";\nimport { Button } from \"src/components\";\nimport { intl, T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\n\nconst showChangePasswordModal = (id: number | \"me\") => {\n\tEasyModal.show(ChangePasswordModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"me\";\n}\nconst ChangePasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst [error, setError] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (values.new !== values.confirm) {\n\t\t\tsetError(<T id=\"error.passwords-must-match\" />);\n\t\t\tsetSubmitting(false);\n\t\t\treturn;\n\t\t}\n\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetError(null);\n\n\t\ttry {\n\t\t\tawait updateAuth(id, values.new, values.current);\n\t\t\tremove();\n\t\t} catch (err: any) {\n\t\t\tsetError(<T id={err.message} />);\n\t\t}\n\t\tsetIsSubmitting(false);\n\t\tsetSubmitting(false);\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\tcurrent: \"\",\n\t\t\t\t\t\tnew: \"\",\n\t\t\t\t\t\tconfirm: \"\",\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{() => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t<T id=\"user.change-password\" />\n\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t<Modal.Body>\n\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!error} onClose={() => setError(null)} dismissible>\n\t\t\t\t\t\t\t\t{error}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t<Field name=\"current\">\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\tid=\"current\"\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"current-password\"\n\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.current && form.touched.current ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: \"user.current-password\",\n\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"current\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.current-password\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t{form.errors.name ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.current && form.touched.current\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.current\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t<Field name=\"new\" validate={validateString(8, 100)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\tid=\"new\"\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.new && form.touched.new ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.new-password\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"new\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.new-password\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t{form.errors.new ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.new && form.touched.new ? form.errors.new : null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t<Field name=\"confirm\" validate={validateString(8, 100)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\tid=\"confirm\"\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.confirm && form.touched.confirm ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.confirm-password\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t{form.errors.confirm ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.confirm && form.touched.confirm\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.confirm\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"confirm\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.confirm-password\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\tclassName=\"ms-auto\"\n\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</Modal>\n\t);\n});\n\nexport { showChangePasswordModal };\n"
  },
  {
    "path": "frontend/src/modals/CustomCertificateModal.tsx",
    "content": "import { IconAlertTriangle } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { type Certificate, createCertificate, uploadCertificate, validateCertificate } from \"src/api/backend\";\nimport { Button } from \"src/components\";\nimport { T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showCustomCertificateModal = () => {\n\tEasyModal.show(CustomCertificateModal);\n};\n\nconst CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {\n\tconst queryClient = useQueryClient();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\ttry {\n\t\t\tconst { niceName, provider, certificate, certificateKey, intermediateCertificate } = values;\n\t\t\tconst formData = new FormData();\n\n\t\t\tformData.append(\"certificate\", certificate);\n\t\t\tformData.append(\"certificate_key\", certificateKey);\n\t\t\tif (intermediateCertificate !== null) {\n\t\t\t\tformData.append(\"intermediate_certificate\", intermediateCertificate);\n\t\t\t}\n\n\t\t\t// Validate\n\t\t\tawait validateCertificate(formData);\n\n\t\t\t// Create certificate, as other without anything else\n\t\t\tconst cert = await createCertificate({ niceName, provider } as Certificate);\n\n\t\t\t// Upload the certificates to the created certificate\n\t\t\tawait uploadCertificate(cert.id, formData);\n\n\t\t\t// Success\n\t\t\tshowObjectSuccess(\"certificate\", \"saved\");\n\t\t\tremove();\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t}\n\n\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\tsetIsSubmitting(false);\n\t\tsetSubmitting(false);\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\tniceName: \"\",\n\t\t\t\t\t\tprovider: \"other\",\n\t\t\t\t\t\tcertificate: null,\n\t\t\t\t\t\tcertificateKey: null,\n\t\t\t\t\t\tintermediateCertificate: null,\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{() => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"certificates.custom\" }} />\n\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-warning\">\n\t\t\t\t\t\t\t\t\t\t<IconAlertTriangle size={16} className=\"me-1\" />\n\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.custom.warning\" />\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<Field name=\"niceName\" validate={validateString(1, 255)}>\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"niceName\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"niceName\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.niceName ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.niceName && form.touched.niceName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.niceName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t<Field name=\"certificateKey\">\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"certificateKey\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificate.custom-certificate-key\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"certificateKey\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.setFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tevent.currentTarget.files?.length\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? event.currentTarget.files[0]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.certificateKey ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.certificateKey && form.touched.certificateKey\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.certificateKey\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t<Field name=\"certificate\">\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"certificate\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificate.custom-certificate\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"certificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.setFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tevent.currentTarget.files?.length\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? event.currentTarget.files[0]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.certificate ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.certificate && form.touched.certificate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.certificate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t<Field name=\"intermediateCertificate\">\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"intermediateCertificate\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificate.custom-intermediate\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"intermediateCertificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.setFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tevent.currentTarget.files?.length\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? event.currentTarget.files[0]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.intermediateCertificate ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.intermediateCertificate &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.intermediateCertificate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.intermediateCertificate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-pink\"\n\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</Modal>\n\t);\n});\n\nexport { showCustomCertificateModal };\n"
  },
  {
    "path": "frontend/src/modals/DNSCertificateModal.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Form, Formik, Field } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { createCertificate } from \"src/api/backend\";\nimport { Button, DNSProviderFields, DomainNamesField } from \"src/components\";\nimport { T } from \"src/locale\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showDNSCertificateModal = () => {\n\tEasyModal.show(DNSCertificateModal);\n};\n\nconst DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {\n\tconst queryClient = useQueryClient();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\ttry {\n\t\t\tawait createCertificate(values);\n\t\t\tshowObjectSuccess(\"certificate\", \"saved\");\n\t\t\tremove();\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t}\n\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\tsetIsSubmitting(false);\n\t\tsetSubmitting(false);\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\tdomainNames: [],\n\t\t\t\t\t\tprovider: \"letsencrypt\",\n\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\tdnsChallenge: true,\n\t\t\t\t\t\t\tkeyType: \"ecdsa\",\n\t\t\t\t\t\t},\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{() => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"lets-encrypt-via-dns\" }} />\n\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />\n\t\t\t\t\t\t\t\t\t<Field name=\"meta.keyType\">\n\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"keyType\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"keyType\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-select\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"rsa\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-rsa\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"ecdsa\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-ecdsa\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t<small className=\"form-text text-muted\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-description\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t<DNSProviderFields />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-pink\"\n\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</Modal>\n\t);\n});\n\nexport { showDNSCertificateModal };\n"
  },
  {
    "path": "frontend/src/modals/DeadHostModal.tsx",
    "content": "import { IconSettings } from \"@tabler/icons-react\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport {\n\tButton,\n\tDomainNamesField,\n\tLoading,\n\tNginxConfigField,\n\tSSLCertificateField,\n\tSSLOptionsFields,\n} from \"src/components\";\nimport { useDeadHost, useSetDeadHost } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showDeadHostModal = (id: number | \"new\") => {\n\tEasyModal.show(DeadHostModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"new\";\n}\nconst DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useDeadHost(id);\n\tconst { mutate: setDeadHost } = useSetDeadHost();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\t...values,\n\t\t};\n\n\t\tsetDeadHost(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"dead-host\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tdomainNames: data?.domainNames,\n\t\t\t\t\t\t\tcertificateId: data?.certificateId,\n\t\t\t\t\t\t\tsslForced: data?.sslForced,\n\t\t\t\t\t\t\tadvancedConfig: data?.advancedConfig,\n\t\t\t\t\t\t\thttp2Support: data?.http2Support,\n\t\t\t\t\t\t\thstsEnabled: data?.hstsEnabled,\n\t\t\t\t\t\t\thstsSubdomains: data?.hstsSubdomains,\n\t\t\t\t\t\t\tmeta: data?.meta || {},\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{() => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id={data?.id ? \"object.edit\" : \"object.add\"} tData={{ object: \"dead-host\" }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t\t\t\t\t\t<ul className=\"nav nav-tabs card-header-tabs\" data-bs-toggle=\"tabs\">\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-details\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link active\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.details\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-ssl\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.ssl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item ms-auto\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-advanced\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconSettings size={20} />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"tab-content\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane active show\" id=\"tab-details\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-ssl\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLCertificateField\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"certificateId\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel=\"ssl-certificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tallowNew\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLOptionsFields color=\"bg-red\" />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-advanced\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<NginxConfigField />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-red\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showDeadHostModal };\n"
  },
  {
    "path": "frontend/src/modals/DeleteConfirmModal.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { Button } from \"src/components\";\nimport { T } from \"src/locale\";\n\ninterface ShowProps {\n\ttitle?: ReactNode;\n\ttTitle?: string;\n\tchildren: ReactNode;\n\tonConfirm: () => Promise<void> | void;\n\tinvalidations?: any[];\n}\n\ninterface Props extends InnerModalProps, ShowProps {}\n\nconst showDeleteConfirmModal = (props: ShowProps) => {\n\tEasyModal.show(DeleteConfirmModal, props);\n};\n\nconst DeleteConfirmModal = EasyModal.create(\n\t({ title, tTitle, children, onConfirm, invalidations, visible, remove }: Props) => {\n\t\tconst queryClient = useQueryClient();\n\t\tconst [error, setError] = useState<ReactNode | null>(null);\n\t\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\t\tconst onSubmit = async () => {\n\t\t\tif (isSubmitting) return;\n\t\t\tsetIsSubmitting(true);\n\t\t\tsetError(null);\n\t\t\ttry {\n\t\t\t\tawait onConfirm();\n\t\t\t\tremove();\n\t\t\t\t// invalidate caches as requested\n\t\t\t\tinvalidations?.forEach((inv) => {\n\t\t\t\t\tqueryClient.invalidateQueries({ queryKey: inv });\n\t\t\t\t});\n\t\t\t} catch (err: any) {\n\t\t\t\tsetError(<T id={err.message} />);\n\t\t\t}\n\t\t\tsetIsSubmitting(false);\n\t\t};\n\n\t\treturn (\n\t\t\t<Modal show={visible} onHide={remove}>\n\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t<Modal.Title>{tTitle ? <T id={tTitle} /> : title ? title : null}</Modal.Title>\n\t\t\t\t</Modal.Header>\n\t\t\t\t<Modal.Body>\n\t\t\t\t\t<Alert variant=\"danger\" show={!!error} onClose={() => setError(null)} dismissible>\n\t\t\t\t\t\t{error}\n\t\t\t\t\t</Alert>\n\t\t\t\t\t<div className=\"text-center mb-3\">\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\taria-label=\"warning icon\"\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\tclassName=\"icon mb-2 text-danger icon-lg\"\n\t\t\t\t\t\t\twidth=\"24\"\n\t\t\t\t\t\t\theight=\"24\"\n\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tstroke-width=\"2\"\n\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t\t\t\t\t<path d=\"M12 9v2m0 4v.01\" />\n\t\t\t\t\t\t\t<path d=\"M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75\" />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"text-center mb-3\">{children}</div>\n\t\t\t\t</Modal.Body>\n\t\t\t\t<Modal.Footer>\n\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\tclassName=\"ms-auto btn-red\"\n\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\tonClick={onSubmit}\n\t\t\t\t\t>\n\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</Modal.Footer>\n\t\t\t</Modal>\n\t\t);\n\t},\n);\n\nexport { showDeleteConfirmModal };\n"
  },
  {
    "path": "frontend/src/modals/EventDetailsModal.tsx",
    "content": "import CodeEditor from \"@uiw/react-textarea-code-editor\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { Button, EventFormatter, GravatarFormatter, Loading } from \"src/components\";\nimport { useAuditLog } from \"src/hooks\";\nimport { T } from \"src/locale\";\n\nconst showEventDetailsModal = (id: number) => {\n\tEasyModal.show(EventDetailsModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number;\n}\nconst EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useAuditLog(id);\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<>\n\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t<T id=\"action.view-details\" />\n\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t<Modal.Body>\n\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t<div className=\"col-md-2\">\n\t\t\t\t\t\t\t\t<GravatarFormatter url={data.user?.avatar || \"\"} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"col-md-10\">\n\t\t\t\t\t\t\t\t<EventFormatter row={data} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<hr className=\"mt-4 mb-3\" />\n\t\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\t\tlanguage=\"json\"\n\t\t\t\t\t\t\t\tpadding={15}\n\t\t\t\t\t\t\t\tdata-color-mode=\"dark\"\n\t\t\t\t\t\t\t\tminHeight={200}\n\t\t\t\t\t\t\t\tindentWidth={2}\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tfontFamily:\n\t\t\t\t\t\t\t\t\t\t\"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"0.3rem\",\n\t\t\t\t\t\t\t\t\tminHeight: \"200px\",\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"var(--tblr-bg-surface-dark)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\treadOnly\n\t\t\t\t\t\t\t\tvalue={JSON.stringify(data.meta, null, 2)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove}>\n\t\t\t\t\t\t\t<T id=\"action.close\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Modal.Footer>\n\t\t\t\t</>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showEventDetailsModal };\n"
  },
  {
    "path": "frontend/src/modals/HTTPCertificateModal.tsx",
    "content": "import { IconAlertTriangle } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Form, Formik, Field } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { createCertificate, testHttpCertificate } from \"src/api/backend\";\nimport { Button, DomainNamesField } from \"src/components\";\nimport { T } from \"src/locale\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showHTTPCertificateModal = () => {\n\tEasyModal.show(HTTPCertificateModal);\n};\n\nconst HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {\n\tconst queryClient = useQueryClient();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [domains, setDomains] = useState([] as string[]);\n\tconst [isTesting, setIsTesting] = useState(false);\n\tconst [testResults, setTestResults] = useState(null as Record<string, string> | null);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\ttry {\n\t\t\tawait createCertificate(values);\n\t\t\tshowObjectSuccess(\"certificate\", \"saved\");\n\t\t\tremove();\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t}\n\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\tsetIsSubmitting(false);\n\t\tsetSubmitting(false);\n\t};\n\n\tconst handleTest = async () => {\n\t\tsetIsTesting(true);\n\t\tsetErrorMsg(null);\n\t\tsetTestResults(null);\n\t\ttry {\n\t\t\tconst result = await testHttpCertificate(domains);\n\t\t\tsetTestResults(result);\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t}\n\t\tsetIsTesting(false);\n\t};\n\n\tconst parseTestResults = () => {\n\t\tconst elms = [];\n\t\tfor (const domain in testResults) {\n\t\t\tconst status = testResults[domain];\n\t\t\tif (status === \"ok\") {\n\t\t\t\telms.push(\n\t\t\t\t\t<p>\n\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-ok\" />\n\t\t\t\t\t</p>,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tif (status === \"no-host\") {\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-not-resolved\" />\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t} else if (status === \"failed\") {\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-failed-to-check\" />\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t} else if (status === \"404\") {\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-404\" />\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t} else if (status === \"wrong-data\") {\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-wrong-data\" />\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t} else if (status.startsWith(\"other:\")) {\n\t\t\t\t\tconst code = status.substring(6);\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> <T id=\"certificates.http.reachability-other\" data={{ code }} />\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\t// This should never happen\n\t\t\t\t\telms.push(\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<strong>{domain}:</strong> ?\n\t\t\t\t\t\t</p>,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn <>{elms}</>;\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\tdomainNames: [],\n\t\t\t\t\t\tprovider: \"letsencrypt\",\n\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\tkeyType: \"ecdsa\",\n\t\t\t\t\t\t},\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{() => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"lets-encrypt-via-http\" }} />\n\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-warning\">\n\t\t\t\t\t\t\t\t\t\t<IconAlertTriangle size={16} className=\"me-1\" />\n\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.http.warning\" />\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<DomainNamesField\n\t\t\t\t\t\t\t\t\t\tonChange={(doms) => {\n\t\t\t\t\t\t\t\t\t\t\tsetDomains(doms);\n\t\t\t\t\t\t\t\t\t\t\tsetTestResults(null);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Field name=\"meta.keyType\">\n\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"keyType\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"keyType\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-select\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"rsa\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-rsa\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"ecdsa\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-ecdsa\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t<small className=\"form-text text-muted\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.key-type-description\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{testResults ? (\n\t\t\t\t\t\t\t\t\t<div className=\"card-footer\">\n\t\t\t\t\t\t\t\t\t\t<h5>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.http.test-results\" />\n\t\t\t\t\t\t\t\t\t\t</h5>\n\t\t\t\t\t\t\t\t\t\t{parseTestResults()}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting || isTesting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<div className=\"ms-auto\">\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tactionType=\"secondary\"\n\t\t\t\t\t\t\t\t\tclassName=\"me-3\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isTesting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting || domains.length === 0}\n\t\t\t\t\t\t\t\t\tonClick={handleTest}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"test\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\tclassName=\"bg-pink\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting || isTesting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</Modal>\n\t);\n});\n\nexport { showHTTPCertificateModal };\n"
  },
  {
    "path": "frontend/src/modals/HelpModal.tsx",
    "content": "import cn from \"classnames\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { useEffect, useState } from \"react\";\nimport Modal from \"react-bootstrap/Modal\";\nimport ReactMarkdown from \"react-markdown\";\nimport { Button } from \"src/components\";\nimport { getLocale, T } from \"src/locale\";\nimport { getHelpFile } from \"src/locale/src/HelpDoc\";\n\ninterface Props extends InnerModalProps {\n\tsection: string;\n\tcolor?: string;\n}\n\nconst showHelpModal = (section: string, color?: string) => {\n\tEasyModal.show(HelpModal, { section, color });\n};\n\nconst HelpModal = EasyModal.create(({ section, color, visible, remove }: Props) => {\n\tconst [markdownText, setMarkdownText] = useState(\"\");\n\tconst lang = getLocale(true);\n\n\tuseEffect(() => {\n\t\ttry {\n\t\t\tconst docFile = getHelpFile(lang, section) as any;\n\t\t\tfetch(docFile)\n\t\t\t\t.then((response) => response.text())\n\t\t\t\t.then(setMarkdownText);\n\t\t} catch (ex: any) {\n\t\t\tsetMarkdownText(`**ERROR:** ${ex.message}`);\n\t\t}\n\t}, [lang, section]);\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Modal.Body>\n\t\t\t\t<ReactMarkdown>{markdownText}</ReactMarkdown>\n\t\t\t</Modal.Body>\n\t\t\t<Modal.Footer>\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\tclassName={cn(\"ms-auto\", color ? `btn-${color}` : null)}\n\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\tonClick={remove}\n\t\t\t\t>\n\t\t\t\t\t<T id=\"action.close\" />\n\t\t\t\t</Button>\n\t\t\t</Modal.Footer>\n\t\t</Modal>\n\t);\n});\n\nexport { showHelpModal };\n"
  },
  {
    "path": "frontend/src/modals/PermissionsModal.module.css",
    "content": ".active {\n    border-color: var(--tblr-orange) !important;\n}\n\n"
  },
  {
    "path": "frontend/src/modals/PermissionsModal.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport cn from \"classnames\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { setPermissions } from \"src/api/backend\";\nimport { Button, Loading } from \"src/components\";\nimport { useUser } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport styles from \"./PermissionsModal.module.css\";\n\nconst showPermissionsModal = (id: number) => {\n\tEasyModal.show(PermissionsModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number;\n}\nconst PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst queryClient = useQueryClient();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst { data, isLoading, error } = useUser(id);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\t\ttry {\n\t\t\tawait setPermissions(id, values);\n\t\t\tremove();\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"users\"] });\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"user\"] });\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t}\n\t\tsetSubmitting(false);\n\t\tsetIsSubmitting(false);\n\t};\n\n\tconst getClasses = (active: boolean) => {\n\t\treturn cn(\"btn\", active ? styles.active : null, {\n\t\t\tactive,\n\t\t\t\"bg-orange-lt\": active,\n\t\t});\n\t};\n\n\t// given the field and clicked permission, intelligently set the value, and\n\t// other values that depends on it.\n\tconst handleChange = (form: any, field: any, perm: string) => {\n\t\tif (field.name === \"proxyHosts\" && perm !== \"hidden\" && form.values.accessLists === \"hidden\") {\n\t\t\tform.setFieldValue(\"accessLists\", \"view\");\n\t\t}\n\t\t// certs are required for proxy and redirection hosts, and streams\n\t\tif (\n\t\t\t[\"proxyHosts\", \"redirectionHosts\", \"deadHosts\", \"streams\"].includes(field.name) &&\n\t\t\tperm !== \"hidden\" &&\n\t\t\tform.values.certificates === \"hidden\"\n\t\t) {\n\t\t\tform.setFieldValue(\"certificates\", \"view\");\n\t\t}\n\n\t\tform.setFieldValue(field.name, perm);\n\t};\n\n\tconst getPermissionButtons = (field: any, form: any) => {\n\t\tconst isManage = field.value === \"manage\";\n\t\tconst isView = field.value === \"view\";\n\t\tconst isHidden = field.value === \"hidden\";\n\n\t\tlet hiddenDisabled = false;\n\t\tif (field.name === \"accessLists\") {\n\t\t\thiddenDisabled = form.values.proxyHosts !== \"hidden\";\n\t\t}\n\t\tif (field.name === \"certificates\") {\n\t\t\thiddenDisabled =\n\t\t\t\tform.values.proxyHosts !== \"hidden\" ||\n\t\t\t\tform.values.redirectionHosts !== \"hidden\" ||\n\t\t\t\tform.values.deadHosts !== \"hidden\" ||\n\t\t\t\tform.values.streams !== \"hidden\";\n\t\t}\n\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<div className=\"btn-group w-100\" role=\"group\">\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\tclassName=\"btn-check\"\n\t\t\t\t\t\tname=\"btn-radio-basic\"\n\t\t\t\t\t\tid={`${field.name}-manage`}\n\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\tvalue=\"manage\"\n\t\t\t\t\t\tchecked={field.value === \"manage\"}\n\t\t\t\t\t\tonChange={() => handleChange(form, field, \"manage\")}\n\t\t\t\t\t/>\n\t\t\t\t\t<label htmlFor={`${field.name}-manage`} className={getClasses(isManage)}>\n\t\t\t\t\t\t<T id=\"permissions.manage\" />\n\t\t\t\t\t</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\tclassName=\"btn-check\"\n\t\t\t\t\t\tname=\"btn-radio-basic\"\n\t\t\t\t\t\tid={`${field.name}-view`}\n\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\tvalue=\"view\"\n\t\t\t\t\t\tchecked={field.value === \"view\"}\n\t\t\t\t\t\tonChange={() => handleChange(form, field, \"view\")}\n\t\t\t\t\t/>\n\t\t\t\t\t<label htmlFor={`${field.name}-view`} className={getClasses(isView)}>\n\t\t\t\t\t\t<T id=\"permissions.view\" />\n\t\t\t\t\t</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\tclassName=\"btn-check\"\n\t\t\t\t\t\tname=\"btn-radio-basic\"\n\t\t\t\t\t\tid={`${field.name}-hidden`}\n\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\tvalue=\"hidden\"\n\t\t\t\t\t\tchecked={field.value === \"hidden\"}\n\t\t\t\t\t\tdisabled={hiddenDisabled}\n\t\t\t\t\t\tonChange={() => handleChange(form, field, \"hidden\")}\n\t\t\t\t\t/>\n\t\t\t\t\t<label htmlFor={`${field.name}-hidden`} className={getClasses(isHidden)}>\n\t\t\t\t\t\t<T id=\"permissions.hidden\" />\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t};\n\n\tconst isAdmin = data?.roles.indexOf(\"admin\") !== -1;\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvisibility: data.permissions?.visibility,\n\t\t\t\t\t\t\taccessLists: data.permissions?.accessLists,\n\t\t\t\t\t\t\tcertificates: data.permissions?.certificates,\n\t\t\t\t\t\t\tdeadHosts: data.permissions?.deadHosts,\n\t\t\t\t\t\t\tproxyHosts: data.permissions?.proxyHosts,\n\t\t\t\t\t\t\tredirectionHosts: data.permissions?.redirectionHosts,\n\t\t\t\t\t\t\tstreams: data.permissions?.streams,\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{() => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id=\"user.set-permissions\" data={{ name: data?.name }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body>\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!error} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<label htmlFor=\"asd\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"permissions.visibility.title\" />\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<Field name=\"visibility\">\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"btn-group w-100\" role=\"group\">\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn-check\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"btn-radio-basic\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tid={`${field.name}-user`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"user\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"user\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() => form.setFieldValue(field.name, \"user\")}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor={`${field.name}-user`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={getClasses(field.value === \"user\")}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"permissions.visibility.user\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn-check\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"btn-radio-basic\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tid={`${field.name}-all`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"all\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"all\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() => form.setFieldValue(field.name, \"all\")}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor={`${field.name}-all`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={getClasses(field.value === \"all\")}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"permissions.visibility.all\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{!isAdmin && (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"proxy-hosts\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"proxyHosts\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"redirection-hosts\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"redirectionHosts\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"dead-hosts\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"deadHosts\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"streams\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"streams\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"access-lists\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"accessLists\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"ignored\" className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates\" />\n\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"certificates\">\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => getPermissionButtons(field, form)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto btn-orange\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showPermissionsModal };\n"
  },
  {
    "path": "frontend/src/modals/ProxyHostModal.tsx",
    "content": "import { IconSettings } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport {\n\tAccessField,\n\tButton,\n\tDomainNamesField,\n\tHasPermission,\n\tLoading,\n\tLocationsFields,\n\tNginxConfigField,\n\tSSLCertificateField,\n\tSSLOptionsFields,\n} from \"src/components\";\nimport { useProxyHost, useSetProxyHost, useUser } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { MANAGE, PROXY_HOSTS } from \"src/modules/Permissions\";\nimport { validateNumber, validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showProxyHostModal = (id: number | \"new\") => {\n\tEasyModal.show(ProxyHostModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"new\";\n}\nconst ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data: currentUser, isLoading: userIsLoading, error: userError } = useUser(\"me\");\n\tconst { data, isLoading, error } = useProxyHost(id);\n\tconst { mutate: setProxyHost } = useSetProxyHost();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\t...values,\n\t\t};\n\n\t\tsetProxyHost(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"proxy-host\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && (error || userError) && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || userError?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading || (userIsLoading && <Loading noLogo />)}\n\t\t\t{!isLoading && !userIsLoading && data && currentUser && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Details tab\n\t\t\t\t\t\t\tdomainNames: data?.domainNames || [],\n\t\t\t\t\t\t\tforwardScheme: data?.forwardScheme || \"http\",\n\t\t\t\t\t\t\tforwardHost: data?.forwardHost || \"\",\n\t\t\t\t\t\t\tforwardPort: data?.forwardPort || undefined,\n\t\t\t\t\t\t\taccessListId: data?.accessListId || 0,\n\t\t\t\t\t\t\tcachingEnabled: data?.cachingEnabled || false,\n\t\t\t\t\t\t\tblockExploits: data?.blockExploits || false,\n\t\t\t\t\t\t\tallowWebsocketUpgrade: data?.allowWebsocketUpgrade || false,\n\t\t\t\t\t\t\t// Locations tab\n\t\t\t\t\t\t\tlocations: data?.locations || [],\n\t\t\t\t\t\t\t// SSL tab\n\t\t\t\t\t\t\tcertificateId: data?.certificateId || 0,\n\t\t\t\t\t\t\tsslForced: data?.sslForced || false,\n\t\t\t\t\t\t\thttp2Support: data?.http2Support || false,\n\t\t\t\t\t\t\thstsEnabled: data?.hstsEnabled || false,\n\t\t\t\t\t\t\thstsSubdomains: data?.hstsSubdomains || false,\n\t\t\t\t\t\t\ttrustForwardedProto: data?.trustForwardedProto || false,\n\t\t\t\t\t\t\t// Advanced tab\n\t\t\t\t\t\t\tadvancedConfig: data?.advancedConfig || \"\",\n\t\t\t\t\t\t\tmeta: data?.meta || {},\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{() => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id={data?.id ? \"object.edit\" : \"object.add\"} tData={{ object: \"proxy-host\" }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t\t\t\t\t\t<ul className=\"nav nav-tabs card-header-tabs\" data-bs-toggle=\"tabs\">\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-details\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link active\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.details\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-locations\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.custom-locations\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-ssl\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.ssl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item ms-auto\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-advanced\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconSettings size={20} />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"tab-content\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane active show\" id=\"tab-details\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardScheme\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-label\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor=\"forwardScheme\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-scheme\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardScheme\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardScheme && form.touched.forwardScheme ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"http\">http</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"https\">https</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardScheme ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardScheme &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardScheme\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardScheme\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-6\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardHost\" validate={validateString(1, 255)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardHost\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"proxy-host.forward-host\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardHost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardHost && form.touched.forwardHost ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"example.com\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardHost ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardHost &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardHost\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardHost\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardPort\" validate={validateNumber(1, 65535)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardPort\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-port\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardPort\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmax={65535}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardPort && form.touched.forwardPort ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"eg: 8081\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardPort ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardPort &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<AccessField />\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"my-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h4 className=\"py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"options\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"divide-y\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"cachingEnabled\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.cache-assets\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"cachingEnabled\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"cachingEnabled\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"form-check-input\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"bg-lime\": field.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"blockExploits\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.block-exploits\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"blockExploits\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"blockExploits\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"form-check-input\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"bg-lime\": field.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"allowWebsocketUpgrade\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.websockets-upgrade\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"allowWebsocketUpgrade\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"allowWebsocketUpgrade\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"form-check-input\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"bg-lime\": field.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-locations\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<LocationsFields initialValues={data?.locations || []} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-ssl\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLCertificateField\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"certificateId\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel=\"ssl-certificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tallowNew\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLOptionsFields color=\"bg-lime\" forProxyHost={true} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-advanced\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<NginxConfigField />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-lime\"\n\t\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showProxyHostModal };\n"
  },
  {
    "path": "frontend/src/modals/RedirectionHostModal.tsx",
    "content": "import { IconSettings } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport {\n\tButton,\n\tDomainNamesField,\n\tLoading,\n\tNginxConfigField,\n\tSSLCertificateField,\n\tSSLOptionsFields,\n} from \"src/components\";\nimport { useRedirectionHost, useSetRedirectionHost } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showRedirectionHostModal = (id: number | \"new\") => {\n\tEasyModal.show(RedirectionHostModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"new\";\n}\nconst RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useRedirectionHost(id);\n\tconst { mutate: setRedirectionHost } = useSetRedirectionHost();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\t...values,\n\t\t};\n\n\t\tsetRedirectionHost(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"redirection-host\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Details tab\n\t\t\t\t\t\t\tdomainNames: data?.domainNames || [],\n\t\t\t\t\t\t\tforwardDomainName: data?.forwardDomainName || \"\",\n\t\t\t\t\t\t\tforwardScheme: data?.forwardScheme || \"auto\",\n\t\t\t\t\t\t\tforwardHttpCode: data?.forwardHttpCode || 301,\n\t\t\t\t\t\t\tpreservePath: data?.preservePath || false,\n\t\t\t\t\t\t\tblockExploits: data?.blockExploits || false,\n\t\t\t\t\t\t\t// SSL tab\n\t\t\t\t\t\t\tcertificateId: data?.certificateId || 0,\n\t\t\t\t\t\t\tsslForced: data?.sslForced || false,\n\t\t\t\t\t\t\thttp2Support: data?.http2Support || false,\n\t\t\t\t\t\t\thstsEnabled: data?.hstsEnabled || false,\n\t\t\t\t\t\t\thstsSubdomains: data?.hstsSubdomains || false,\n\t\t\t\t\t\t\t// Advanced tab\n\t\t\t\t\t\t\tadvancedConfig: data?.advancedConfig || \"\",\n\t\t\t\t\t\t\tmeta: data?.meta || {},\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{() => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid={data?.id ? \"object.edit\" : \"object.add\"}\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"redirection-host\" }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t\t\t\t\t\t<ul className=\"nav nav-tabs card-header-tabs\" data-bs-toggle=\"tabs\">\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-details\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link active\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.details\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-ssl\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.ssl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item ms-auto\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-advanced\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconSettings size={20} />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"tab-content\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane active show\" id=\"tab-details\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardScheme\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-label\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor=\"forwardScheme\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-scheme\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardScheme\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardScheme && form.touched.forwardScheme ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"auto\"><T id=\"auto\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"http\">http</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"https\">https</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardScheme ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardScheme &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardScheme\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardScheme\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-8\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"forwardDomainName\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvalidate={validateString(1, 255)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-label\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor=\"forwardDomainName\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"redirection-host.forward-domain\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardDomainName\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardDomainName && form.touched.forwardDomainName ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"example.com\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardDomainName ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardDomainName &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardDomainName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardDomainName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardHttpCode\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"forwardHttpCode\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"redirection-host.forward-http-code\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardHttpCode\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardHttpCode && form.touched.forwardHttpCode ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"300\"><T id=\"redirection-hosts.http-code.300\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"301\"><T id=\"redirection-hosts.http-code.301\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"302\"><T id=\"redirection-hosts.http-code.302\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"303\"><T id=\"redirection-hosts.http-code.303\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"307\"><T id=\"redirection-hosts.http-code.307\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"308\"><T id=\"redirection-hosts.http-code.308\" /></option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardHttpCode ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardHttpCode &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardHttpCode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardHttpCode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"my-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h4 className=\"py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"options\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"divide-y\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"preservePath\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.preserve-path\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"preservePath\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"preservePath\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"form-check-input\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"bg-yellow\": field.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"blockExploits\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.block-exploits\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"blockExploits\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"blockExploits\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"form-check-input\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"bg-yellow\": field.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-ssl\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLCertificateField\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"certificateId\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel=\"ssl-certificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tallowNew\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLOptionsFields color=\"bg-yellow\" />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-advanced\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<NginxConfigField />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-yellow\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showRedirectionHostModal };\n"
  },
  {
    "path": "frontend/src/modals/RenewCertificateModal.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { renewCertificate } from \"src/api/backend\";\nimport { Button, Loading } from \"src/components\";\nimport { useCertificate } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showObjectSuccess } from \"src/notifications\";\n\ninterface Props extends InnerModalProps {\n\tid: number;\n}\n\nconst showRenewCertificateModal = (id: number) => {\n\tEasyModal.show(RenewCertificateModal, { id });\n};\n\nconst RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst queryClient = useQueryClient();\n\tconst { data, isLoading, error } = useCertificate(id);\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isFresh, setIsFresh] = useState(true);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tuseEffect(() => {\n\t\tif (!data || !isFresh || isSubmitting) return;\n\t\tsetIsFresh(false);\n\t\tsetIsSubmitting(true);\n\n\t\trenewCertificate(id)\n\t\t\t.then(() => {\n\t\t\t\tshowObjectSuccess(\"certificate\", \"renewed\");\n\t\t\t\tqueryClient.invalidateQueries({ queryKey: [\"certificates\"] });\n\t\t\t\tremove();\n\t\t\t})\n\t\t\t.catch((err: any) => {\n\t\t\t\tsetErrorMsg(<T id={err.message} />);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t});\n\t}, [id, data, isFresh, isSubmitting, remove, queryClient]);\n\n\treturn (\n\t\t<Modal show={visible} onHide={isSubmitting ? undefined : remove}>\n\t\t\t<Modal.Header closeButton={!isSubmitting}>\n\t\t\t\t<Modal.Title>\n\t\t\t\t\t<T id=\"certificate.renew\" />\n\t\t\t\t</Modal.Title>\n\t\t\t</Modal.Header>\n\t\t\t<Modal.Body>\n\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg}>\n\t\t\t\t\t{errorMsg}\n\t\t\t\t</Alert>\n\t\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t\t{!isLoading && error && (\n\t\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t\t</Alert>\n\t\t\t\t)}\n\t\t\t\t{data && isSubmitting && !errorMsg ? <p className=\"text-center mt-3\">Please wait ...</p> : null}\n\t\t\t</Modal.Body>\n\t\t\t<Modal.Footer>\n\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t<T id=\"action.close\" />\n\t\t\t\t</Button>\n\t\t\t</Modal.Footer>\n\t\t</Modal>\n\t);\n});\n\nexport { showRenewCertificateModal };\n"
  },
  {
    "path": "frontend/src/modals/SetPasswordModal.tsx",
    "content": "import EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { generate } from \"generate-password-browser\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { updateAuth } from \"src/api/backend\";\nimport { Button } from \"src/components\";\nimport { intl, T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\n\nconst showSetPasswordModal = (id: number) => {\n\tEasyModal.show(SetPasswordModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number;\n}\nconst SetPasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst [error, setError] = useState<ReactNode | null>(null);\n\tconst [showPassword, setShowPassword] = useState(false);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetError(null);\n\t\ttry {\n\t\t\tawait updateAuth(id, values.new);\n\t\t\tremove();\n\t\t} catch (err: any) {\n\t\t\tsetError(<T id={err.message} />);\n\t\t}\n\t\tsetIsSubmitting(false);\n\t\tsetSubmitting(false);\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\tnew: \"\",\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{() => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t<T id=\"user.set-password\" />\n\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t<Modal.Body>\n\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!error} onClose={() => setError(null)} dismissible>\n\t\t\t\t\t\t\t\t{error}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t<Field name=\"new\" validate={validateString(8, 100)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-end\">\n\t\t\t\t\t\t\t\t\t\t\t\t<small>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.setFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgenerate({\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlength: 12,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnumbers: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetShowPassword(true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"password.generate\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t&mdash;{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-xs\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetShowPassword(!showPassword);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id={showPassword ? \"password.hide\" : \"password.show\"} />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"new\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype={showPassword ? \"text\" : \"password\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.new && form.touched.new ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.new-password\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"new\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.new-password\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.new ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.new && form.touched.new ? form.errors.new : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\tclassName=\"ms-auto\"\n\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</Modal>\n\t);\n});\n\nexport { showSetPasswordModal };\n"
  },
  {
    "path": "frontend/src/modals/StreamModal.tsx",
    "content": "import EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { Button, Loading, SSLCertificateField, SSLOptionsFields } from \"src/components\";\nimport { useSetStream, useStream } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport { validateNumber, validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showStreamModal = (id: number | \"new\") => {\n\tEasyModal.show(StreamModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"new\";\n}\nconst StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useStream(id);\n\tconst { mutate: setStream } = useSetStream();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\t...values,\n\t\t};\n\n\t\tsetStream(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"stream\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{isLoading && <Loading noLogo />}\n\t\t\t{!isLoading && data && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tincomingPort: data?.incomingPort,\n\t\t\t\t\t\t\tforwardingHost: data?.forwardingHost,\n\t\t\t\t\t\t\tforwardingPort: data?.forwardingPort,\n\t\t\t\t\t\t\ttcpForwarding: data?.tcpForwarding,\n\t\t\t\t\t\t\tudpForwarding: data?.udpForwarding,\n\t\t\t\t\t\t\tcertificateId: data?.certificateId,\n\t\t\t\t\t\t\tmeta: data?.meta || {},\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{({ setFieldValue }: any) => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id={data?.id ? \"object.edit\" : \"object.add\"} tData={{ object: \"stream\" }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body className=\"p-0\">\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\n\t\t\t\t\t\t\t\t<div className=\"card m-0 border-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t\t\t\t\t\t<ul className=\"nav nav-tabs card-header-tabs\" data-bs-toggle=\"tabs\">\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-details\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link active\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.details\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li className=\"nav-item\" role=\"presentation\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#tab-ssl\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"nav-link\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected=\"false\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"column.ssl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"tab-content\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane active show\" id=\"tab-details\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"incomingPort\" validate={validateNumber(1, 65535)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"incomingPort\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"stream.incoming-port\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"incomingPort\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmax={65535}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.incomingPort && form.touched.incomingPort ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"eg: 8080\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.incomingPort ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.incomingPort &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.incomingPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.incomingPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-8\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"forwardingHost\" validate={validateString(1, 255)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-label\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor=\"forwardingHost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"stream.forward-host\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardingHost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardingHost && form.touched.forwardingHost ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"stream.forward-host.placeholder\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardingHost ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardingHost &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardingHost\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardingHost\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-md-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"forwardingPort\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvalidate={validateNumber(1, 65535)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-label\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thtmlFor=\"forwardingPort\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.forward-port\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"forwardingPort\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmax={65535}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.forwardingPort && form.touched.forwardingPort ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"eg: 8081\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardingPort ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.forwardingPort &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tform.touched.forwardingPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.forwardingPort\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"my-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"host.flags.protocols\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"divide-y\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"tcpForwarding\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"streams.tcp\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"tcpForwarding\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"tcpForwarding\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-check-input\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.target.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (!e.target.checked) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"udpForwarding\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"udpForwarding\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"streams.udp\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"udpForwarding\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"udpForwarding\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-check-input\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfield.name,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.target.checked,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (!e.target.checked) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetFieldValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"tcpForwarding\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"tab-pane\" id=\"tab-ssl\" role=\"tabpanel\">\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLCertificateField\n\t\t\t\t\t\t\t\t\t\t\t\t\tname=\"certificateId\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel=\"ssl-certificate\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tallowNew\n\t\t\t\t\t\t\t\t\t\t\t\t\tforHttp={false}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<SSLOptionsFields\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor=\"bg-blue\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tforHttp={false}\n\t\t\t\t\t\t\t\t\t\t\t\t\tforceDNSForNew\n\t\t\t\t\t\t\t\t\t\t\t\t\trequireDomainNames\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showStreamModal };\n"
  },
  {
    "path": "frontend/src/modals/TwoFactorModal.tsx",
    "content": "import EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useCallback, useEffect, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport {\n\tdisable2FA,\n\tenable2FA,\n\tget2FAStatus,\n\tregenerateBackupCodes,\n\tstart2FASetup,\n} from \"src/api/backend\";\nimport { Button } from \"src/components\";\nimport { T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\n\ntype Step = \"loading\" | \"status\" | \"setup\" | \"verify\" | \"backup\" | \"disable\";\n\nconst showTwoFactorModal = (id: number | \"me\") => {\n\tEasyModal.show(TwoFactorModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"me\";\n}\n\nconst TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst [error, setError] = useState<ReactNode | null>(null);\n\tconst [step, setStep] = useState<Step>(\"loading\");\n\tconst [isEnabled, setIsEnabled] = useState(false);\n\tconst [backupCodesRemaining, setBackupCodesRemaining] = useState(0);\n\tconst [setupData, setSetupData] = useState<{ secret: string; otpauthUrl: string } | null>(null);\n\tconst [backupCodes, setBackupCodes] = useState<string[]>([]);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst loadStatus = useCallback(async () => {\n\t\ttry {\n\t\t\tconst status = await get2FAStatus(id);\n\t\t\tsetIsEnabled(status.enabled);\n\t\t\tsetBackupCodesRemaining(status.backupCodesRemaining);\n\t\t\tsetStep(\"status\");\n\t\t} catch (err: any) {\n\t\t\tsetError(err.message || \"Failed to load 2FA status\");\n\t\t\tsetStep(\"status\");\n\t\t}\n\t}, [id]);\n\n\tuseEffect(() => {\n\t\tloadStatus();\n\t}, [loadStatus]);\n\n\tconst handleStartSetup = async () => {\n\t\tsetError(null);\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tconst data = await start2FASetup(id);\n\t\t\tsetSetupData(data);\n\t\t\tsetStep(\"setup\");\n\t\t} catch (err: any) {\n\t\t\tsetError(err.message || \"Failed to start 2FA setup\");\n\t\t}\n\t\tsetIsSubmitting(false);\n\t};\n\n\tconst handleVerify = async (values: { code: string }) => {\n\t\tsetError(null);\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tconst result = await enable2FA(id, values.code);\n\t\t\tsetBackupCodes(result.backupCodes);\n\t\t\tsetStep(\"backup\");\n\t\t} catch (err: any) {\n\t\t\tsetError(err.message || \"Failed to enable 2FA\");\n\t\t}\n\t\tsetIsSubmitting(false);\n\t};\n\n\tconst handleDisable = async (values: { code: string }) => {\n\t\tsetError(null);\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tawait disable2FA(id, values.code);\n\t\t\tsetIsEnabled(false);\n\t\t\tsetStep(\"status\");\n\t\t} catch (err: any) {\n\t\t\tsetError(err.message || \"Failed to disable 2FA\");\n\t\t}\n\t\tsetIsSubmitting(false);\n\t};\n\n\tconst handleRegenerateBackup = async (values: { code: string }) => {\n\t\tsetError(null);\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tconst result = await regenerateBackupCodes(id, values.code);\n\t\t\tsetBackupCodes(result.backupCodes);\n\t\t\tsetStep(\"backup\");\n\t\t} catch (err: any) {\n\t\t\tsetError(err.message || \"Failed to regenerate backup codes\");\n\t\t}\n\t\tsetIsSubmitting(false);\n\t};\n\n\tconst handleBackupDone = () => {\n\t\tsetIsEnabled(true);\n\t\tsetBackupCodes([]);\n\t\tloadStatus();\n\t};\n\n\tconst renderContent = () => {\n\t\tif (step === \"loading\") {\n\t\t\treturn (\n\t\t\t\t<div className=\"text-center py-4\">\n\t\t\t\t\t<div className=\"spinner-border\" role=\"status\">\n\t\t\t\t\t\t<span className=\"visually-hidden\">Loading...</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (step === \"status\") {\n\t\t\treturn (\n\t\t\t\t<div className=\"py-2\">\n\t\t\t\t\t<div className=\"mb-4\">\n\t\t\t\t\t\t<div className=\"d-flex align-items-center justify-content-between mb-2\">\n\t\t\t\t\t\t\t<span className=\"fw-bold\">\n\t\t\t\t\t\t\t\t<T id=\"2fa.status\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className={`badge text-white ${isEnabled ? \"bg-success\" : \"bg-secondary\"}`}>\n\t\t\t\t\t\t\t\t{isEnabled ? <T id=\"2fa.enabled\" /> : <T id=\"2fa.disabled\" />}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{isEnabled && (\n\t\t\t\t\t\t\t<p className=\"text-muted small mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"2fa.backup-codes-remaining\" data={{ count: backupCodesRemaining }} />\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t{!isEnabled ? (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\t\tcolor=\"azure\"\n\t\t\t\t\t\t\tonClick={handleStartSetup}\n\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<T id=\"2fa.enable\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div className=\"d-flex flex-column gap-2\">\n\t\t\t\t\t\t\t<Button fullWidth onClick={() => setStep(\"disable\")}>\n\t\t\t\t\t\t\t\t<T id=\"2fa.disable\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button fullWidth onClick={() => setStep(\"verify\")}>\n\t\t\t\t\t\t\t\t<T id=\"2fa.regenerate-backup\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (step === \"setup\" && setupData) {\n\t\t\treturn (\n\t\t\t\t<div className=\"py-2\">\n\t\t\t\t\t<p className=\"text-muted mb-3\">\n\t\t\t\t\t\t<T id=\"2fa.setup-instructions\" />\n\t\t\t\t\t</p>\n\t\t\t\t\t<div className=\"text-center mb-3\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(setupData.otpauthUrl)}`}\n\t\t\t\t\t\t\talt=\"QR Code\"\n\t\t\t\t\t\t\tclassName=\"img-fluid\"\n\t\t\t\t\t\t\tstyle={{ maxWidth: \"200px\" }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<label className=\"mb-3 d-block\">\n\t\t\t\t\t\t<span className=\"form-label small text-muted\">\n\t\t\t\t\t\t\t<T id=\"2fa.secret-key\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclassName=\"form-control font-monospace\"\n\t\t\t\t\t\t\tvalue={setupData.secret}\n\t\t\t\t\t\t\treadOnly\n\t\t\t\t\t\t\tonClick={(e) => (e.target as HTMLInputElement).select()}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</label>\n\t\t\t\t\t<Formik initialValues={{ code: \"\" }} onSubmit={handleVerify}>\n\t\t\t\t\t\t{() => (\n\t\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t\t<Field name=\"code\" validate={validateString(6, 6)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<label className=\"mb-3 d-block\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.enter-code\" />\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\tinputMode=\"numeric\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"one-time-code\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.code && form.touched.code ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"000000\"\n\t\t\t\t\t\t\t\t\t\t\t\tmaxLength={6}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.code}</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t<div className=\"d-flex gap-2\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\t\t\t\t\tonClick={() => setStep(\"status\")}\n\t\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button type=\"submit\" fullWidth color=\"azure\" isLoading={isSubmitting}>\n\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.verify-enable\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Formik>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (step === \"backup\") {\n\t\t\treturn (\n\t\t\t\t<div className=\"py-2\">\n\t\t\t\t\t<Alert variant=\"warning\">\n\t\t\t\t\t\t<T id=\"2fa.backup-warning\" />\n\t\t\t\t\t</Alert>\n\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t<div className=\"row g-2\">\n\t\t\t\t\t\t\t{backupCodes.map((code, index) => (\n\t\t\t\t\t\t\t\t<div key={index} className=\"col-6\">\n\t\t\t\t\t\t\t\t\t<code className=\"d-block p-2 bg-light rounded text-center\">{code}</code>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Button fullWidth color=\"azure\" onClick={handleBackupDone}>\n\t\t\t\t\t\t<T id=\"2fa.done\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (step === \"disable\") {\n\t\t\treturn (\n\t\t\t\t<div className=\"py-2\">\n\t\t\t\t\t<Alert variant=\"warning\">\n\t\t\t\t\t\t<T id=\"2fa.disable-warning\" />\n\t\t\t\t\t</Alert>\n\t\t\t\t\t<Formik initialValues={{ code: \"\" }} onSubmit={handleDisable}>\n\t\t\t\t\t\t{() => (\n\t\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t\t<Field name=\"code\" validate={validateString(6, 6)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<label className=\"mb-3 d-block\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.enter-code-disable\" />\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\tinputMode=\"numeric\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"one-time-code\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.code && form.touched.code ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"000000\"\n\t\t\t\t\t\t\t\t\t\t\t\tmaxLength={6}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.code}</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t<div className=\"d-flex gap-2\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\t\t\t\t\tonClick={() => setStep(\"status\")}\n\t\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button type=\"submit\" fullWidth color=\"red\" isLoading={isSubmitting}>\n\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.disable-confirm\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Formik>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (step === \"verify\") {\n\t\t\treturn (\n\t\t\t\t<div className=\"py-2\">\n\t\t\t\t\t<p className=\"text-muted mb-3\">\n\t\t\t\t\t\t<T id=\"2fa.regenerate-instructions\" />\n\t\t\t\t\t</p>\n\t\t\t\t\t<Formik initialValues={{ code: \"\" }} onSubmit={handleRegenerateBackup}>\n\t\t\t\t\t\t{() => (\n\t\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t\t<Field name=\"code\" validate={validateString(6, 6)}>\n\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t<label className=\"mb-3 d-block\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.enter-code\" />\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\tinputMode=\"numeric\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"one-time-code\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.code && form.touched.code ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"000000\"\n\t\t\t\t\t\t\t\t\t\t\t\tmaxLength={6}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.code}</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t<div className=\"d-flex gap-2\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\t\t\t\t\tonClick={() => setStep(\"status\")}\n\t\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button type=\"submit\" fullWidth color=\"azure\" isLoading={isSubmitting}>\n\t\t\t\t\t\t\t\t\t\t<T id=\"2fa.regenerate\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Formik>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\treturn null;\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t<Modal.Header closeButton>\n\t\t\t\t<Modal.Title>\n\t\t\t\t\t<T id=\"2fa.title\" />\n\t\t\t\t</Modal.Title>\n\t\t\t</Modal.Header>\n\t\t\t<Modal.Body>\n\t\t\t\t<Alert variant=\"danger\" show={!!error} onClose={() => setError(null)} dismissible>\n\t\t\t\t\t{error}\n\t\t\t\t</Alert>\n\t\t\t\t{renderContent()}\n\t\t\t</Modal.Body>\n\t\t</Modal>\n\t);\n});\n\nexport { showTwoFactorModal };\n"
  },
  {
    "path": "frontend/src/modals/UserModal.tsx",
    "content": "import EasyModal, { type InnerModalProps } from \"ez-modal-react\";\nimport { Field, Form, Formik } from \"formik\";\nimport { useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport Modal from \"react-bootstrap/Modal\";\nimport { Button, Loading } from \"src/components\";\nimport { useSetUser, useUser } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport { validateEmail, validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nconst showUserModal = (id: number | \"me\" | \"new\") => {\n\tEasyModal.show(UserModal, { id });\n};\n\ninterface Props extends InnerModalProps {\n\tid: number | \"me\" | \"new\";\n}\nconst UserModal = EasyModal.create(({ id, visible, remove }: Props) => {\n\tconst { data, isLoading, error } = useUser(id);\n\tconst { data: currentUser, isLoading: currentIsLoading } = useUser(\"me\");\n\tconst { mutate: setUser } = useSetUser();\n\tconst [errorMsg, setErrorMsg] = useState<string | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst { ...payload } = {\n\t\t\tid: id === \"new\" ? undefined : id,\n\t\t\troles: [],\n\t\t\t...values,\n\t\t};\n\n\t\tif (data?.id === currentUser?.id) {\n\t\t\t// Prevent user from locking themselves out\n\t\t\tdelete payload.isDisabled;\n\t\t\tdelete payload.roles;\n\t\t} else if (payload.isAdmin) {\n\t\t\tpayload.roles = [\"admin\"];\n\t\t}\n\n\t\t// this isn't a real field, just for the form\n\t\tdelete payload.isAdmin;\n\n\t\tsetUser(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(err.message),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"user\", \"saved\");\n\t\t\t\tremove();\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\treturn (\n\t\t<Modal show={visible} onHide={remove}>\n\t\t\t{!isLoading && error && (\n\t\t\t\t<Alert variant=\"danger\" className=\"m-3\">\n\t\t\t\t\t{error?.message || \"Unknown error\"}\n\t\t\t\t</Alert>\n\t\t\t)}\n\t\t\t{(isLoading || currentIsLoading) && <Loading noLogo />}\n\t\t\t{!isLoading && !currentIsLoading && data && currentUser && (\n\t\t\t\t<Formik\n\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: data?.name,\n\t\t\t\t\t\t\tnickname: data?.nickname,\n\t\t\t\t\t\t\temail: data?.email,\n\t\t\t\t\t\t\tisAdmin: data?.roles?.includes(\"admin\"),\n\t\t\t\t\t\t\tisDisabled: data?.isDisabled,\n\t\t\t\t\t\t} as any\n\t\t\t\t\t}\n\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t>\n\t\t\t\t\t{() => (\n\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t<Modal.Header closeButton>\n\t\t\t\t\t\t\t\t<Modal.Title>\n\t\t\t\t\t\t\t\t\t<T id={data?.id ? \"object.edit\" : \"object.add\"} tData={{ object: \"user\" }} />\n\t\t\t\t\t\t\t\t</Modal.Title>\n\t\t\t\t\t\t\t</Modal.Header>\n\t\t\t\t\t\t\t<Modal.Body>\n\t\t\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t<div className=\"row\">\n\t\t\t\t\t\t\t\t\t<div className=\"col-lg-6\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"name\" validate={validateString(1, 50)}>\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"name\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.name && form.touched.name ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.full-name\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"name\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.full-name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name && form.touched.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"col-lg-6\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t<Field name=\"nickname\" validate={validateString(1, 30)}>\n\t\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"nickname\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.nickname && form.touched.nickname ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.nickname\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"nickname\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.nickname\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.nickname ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.nickname && form.touched.nickname\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.nickname\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<Field name=\"email\" validate={validateEmail()}>\n\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.email && form.touched.email ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"email-address\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"email\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"email-address\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.email ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.email && form.touched.email\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.email\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{currentUser && data && currentUser?.id !== data?.id ? (\n\t\t\t\t\t\t\t\t\t<div className=\"my-3\">\n\t\t\t\t\t\t\t\t\t\t<h4 className=\"py-2\">\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"options\" />\n\t\t\t\t\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t\t\t\t\t<div className=\"divide-y\">\n\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"isAdmin\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"role.admin\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"isAdmin\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"isAdmin\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-check-input\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"row\" htmlFor=\"isDisabled\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"disabled\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Field name=\"isDisabled\" type=\"checkbox\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{({ field }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<label className=\"form-check form-check-single form-switch\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"isDisabled\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-check-input\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</Modal.Body>\n\t\t\t\t\t\t\t<Modal.Footer>\n\t\t\t\t\t\t\t\t<Button data-bs-dismiss=\"modal\" onClick={remove} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tclassName=\"ms-auto btn-orange\"\n\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Modal.Footer>\n\t\t\t\t\t\t</Form>\n\t\t\t\t\t)}\n\t\t\t\t</Formik>\n\t\t\t)}\n\t\t</Modal>\n\t);\n});\n\nexport { showUserModal };\n"
  },
  {
    "path": "frontend/src/modals/index.ts",
    "content": "export * from \"./AccessListModal\";\nexport * from \"./ChangePasswordModal\";\nexport * from \"./CustomCertificateModal\";\nexport * from \"./DeadHostModal\";\nexport * from \"./DeleteConfirmModal\";\nexport * from \"./DNSCertificateModal\";\nexport * from \"./EventDetailsModal\";\nexport * from \"./HelpModal\";\nexport * from \"./HTTPCertificateModal\";\nexport * from \"./PermissionsModal\";\nexport * from \"./ProxyHostModal\";\nexport * from \"./RedirectionHostModal\";\nexport * from \"./RenewCertificateModal\";\nexport * from \"./SetPasswordModal\";\nexport * from \"./StreamModal\";\nexport * from \"./TwoFactorModal\";\nexport * from \"./UserModal\";\n"
  },
  {
    "path": "frontend/src/modules/AuthStore.ts",
    "content": "import { getUnixTime, parseISO } from \"date-fns\";\nimport type { TokenResponse } from \"src/api/backend\";\n\nexport const TOKEN_KEY = \"authentications\";\n\nexport class AuthStore {\n\t// Get all tokens from stack\n\tget tokens() {\n\t\tconst t = localStorage.getItem(TOKEN_KEY);\n\t\tlet tokens = [];\n\t\tif (t !== null) {\n\t\t\ttry {\n\t\t\t\ttokens = JSON.parse(t);\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\"Failed to parse tokens from localStorage\", e);\n\t\t\t}\n\t\t}\n\t\treturn tokens;\n\t}\n\n\t// Get last token from stack\n\tget token() {\n\t\tconst t = this.tokens;\n\t\tif (t.length) {\n\t\t\treturn t[t.length - 1];\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Get expires from last token\n\tget expires() {\n\t\tconst t = this.token;\n\t\tif (t && typeof t.expires !== \"undefined\") {\n\t\t\tconst expires = Number(t.expires);\n\t\t\tif (expires && !Number.isNaN(expires)) {\n\t\t\t\treturn expires;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Filter out invalid tokens and return true if we find one that is valid\n\t// hasActiveToken() {\n\t// \tconst t = this.tokens;\n\t// \treturn t.length > 0;\n\t// }\n\t// Start from the END of the stack and work backwards\n\thasActiveToken() {\n\t\tconst t = this.tokens;\n\t\tif (!t.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst now = Math.round(Date.now() / 1000);\n\t\tconst oneMinuteBuffer = 60;\n\t\tfor (let i = t.length - 1; i >= 0; i--) {\n\t\t\tconst dte = getUnixTime(parseISO(t[i].expires));\n\t\t\tconst valid = dte - oneMinuteBuffer > now;\n\t\t\tif (valid) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tthis.drop();\n\t\t}\n\t\treturn false;\n\t}\n\n\t// Set a single token on the stack\n\tset({ token, expires }: TokenResponse) {\n\t\tlocalStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));\n\t}\n\n\t// Add a token to the END of the stack\n\tadd({ token, expires }: TokenResponse) {\n\t\tconst t = this.tokens;\n\t\tt.push({ token, expires });\n\t\tlocalStorage.setItem(TOKEN_KEY, JSON.stringify(t));\n\t}\n\n\t// Drop a token from the END of the stack\n\tdrop() {\n\t\tconst t = this.tokens;\n\t\tt.splice(-1, 1);\n\t\tlocalStorage.setItem(TOKEN_KEY, JSON.stringify(t));\n\t}\n\n\tclear() {\n\t\tlocalStorage.removeItem(TOKEN_KEY);\n\t}\n\n\tcount() {\n\t\treturn this.tokens.length;\n\t}\n}\n\nexport default new AuthStore();\n"
  },
  {
    "path": "frontend/src/modules/Permissions.ts",
    "content": "import type { UserPermissions } from \"src/api/backend\";\n\nexport const ADMIN = \"admin\";\nexport const VISIBILITY = \"visibility\";\nexport const PROXY_HOSTS = \"proxyHosts\";\nexport const REDIRECTION_HOSTS = \"redirectionHosts\";\nexport const DEAD_HOSTS = \"deadHosts\";\nexport const STREAMS = \"streams\";\nexport const CERTIFICATES = \"certificates\";\nexport const ACCESS_LISTS = \"accessLists\";\n\nexport const MANAGE = \"manage\";\nexport const VIEW = \"view\";\nexport const HIDDEN = \"hidden\";\n\nexport const ALL = \"all\";\nexport const USER = \"user\";\n\nexport type Section =\n\t| typeof ADMIN\n\t| typeof VISIBILITY\n\t| typeof PROXY_HOSTS\n\t| typeof REDIRECTION_HOSTS\n\t| typeof DEAD_HOSTS\n\t| typeof STREAMS\n\t| typeof CERTIFICATES\n\t| typeof ACCESS_LISTS;\n\nexport type Permission = typeof MANAGE | typeof VIEW;\n\nconst hasPermission = (\n\tsection: Section,\n\tperm: Permission,\n\tuserPerms: UserPermissions | undefined,\n\troles: string[] | undefined,\n): boolean => {\n\tif (!userPerms) return false;\n\tif (isAdmin(roles)) return true;\n\tconst acceptable = [MANAGE, perm];\n\t// @ts-expect-error 7053\n\tconst v = typeof userPerms[section] !== \"undefined\" ? userPerms[section] : HIDDEN;\n\treturn acceptable.indexOf(v) !== -1;\n};\n\nconst isAdmin = (roles: string[] | undefined): boolean => {\n\treturn roles?.includes(\"admin\") || false;\n};\n\nexport { hasPermission, isAdmin };\n"
  },
  {
    "path": "frontend/src/modules/Validations.tsx",
    "content": "import { intl } from \"src/locale\";\n\nconst validateString = (minLength = 0, maxLength = 0) => {\n\tif (minLength <= 0 && maxLength <= 0) {\n\t\t// this doesn't require translation\n\t\tconsole.error(\"validateString() must be called with a min or max or both values in order to work!\");\n\t}\n\n\treturn (value: string): string | undefined => {\n\t\tif (minLength && (typeof value === \"undefined\" || !value.length)) {\n\t\t\treturn intl.formatMessage({ id: \"error.required\" });\n\t\t}\n\t\tif (minLength && value.length < minLength) {\n\t\t\treturn intl.formatMessage({ id: \"error.min-character-length\" }, { min: minLength });\n\t\t}\n\t\tif (maxLength && (typeof value === \"undefined\" || value.length > maxLength)) {\n\t\t\treturn intl.formatMessage({ id: \"error.max-character-length\" }, { max: maxLength });\n\t\t}\n\t};\n};\n\nconst validateNumber = (min = -1, max = -1) => {\n\tif (min === -1 && max === -1) {\n\t\t// this doesn't require translation\n\t\tconsole.error(\"validateNumber() must be called with a min or max or both values in order to work!\");\n\t}\n\n\treturn (value: string): string | undefined => {\n\t\tconst int: number = +value;\n\t\tif (min > -1 && !int) {\n\t\t\treturn intl.formatMessage({ id: \"error.required\" });\n\t\t}\n\t\tif (min > -1 && int < min) {\n\t\t\treturn intl.formatMessage({ id: \"error.minimum\" }, { min });\n\t\t}\n\t\tif (max > -1 && int > max) {\n\t\t\treturn intl.formatMessage({ id: \"error.maximum\" }, { max });\n\t\t}\n\t};\n};\n\nconst validateEmail = () => {\n\treturn (value: string): string | undefined => {\n\t\tif (!value.length) {\n\t\t\treturn intl.formatMessage({ id: \"error.required\" });\n\t\t}\n\t\tif (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i.test(value)) {\n\t\t\treturn intl.formatMessage({ id: \"error.invalid-email\" });\n\t\t}\n\t};\n};\n\nconst validateDomain = (allowWildcards = false) => {\n\treturn (d: string): boolean => {\n\t\tconst dom = d.trim().toLowerCase();\n\n\t\tif (dom.length < 3) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Prevent wildcards\n\t\tif (!allowWildcards && dom.indexOf(\"*\") !== -1) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Prevent duplicate * in domain\n\t\tif ((dom.match(/\\*/g) || []).length > 1) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Prevent some invalid characters\n\t\tif ((dom.match(/(@|,|!|&|\\$|#|%|\\^|\\(|\\))/g) || []).length > 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// This will match *.com type domains,\n\t\treturn dom.match(/\\*\\.[^.]+$/m) === null;\n\t};\n};\n\nconst validateDomains = (allowWildcards = false, maxDomains?: number) => {\n\tconst vDom = validateDomain(allowWildcards);\n\n\treturn (value?: string[]): string | undefined => {\n\t\tif (!value?.length) {\n\t\t\treturn intl.formatMessage({ id: \"error.required\" });\n\t\t}\n\n\t\t// Deny if the list of domains is hit\n\t\tif (maxDomains && value?.length >= maxDomains) {\n\t\t\treturn intl.formatMessage({ id: \"error.max-domains\" }, { max: maxDomains });\n\t\t}\n\n\t\t// validate each domain\n\t\tfor (let i = 0; i < value?.length; i++) {\n\t\t\tif (!vDom(value[i])) {\n\t\t\t\treturn intl.formatMessage({ id: \"error.invalid-domain\" }, { domain: value[i] });\n\t\t\t}\n\t\t}\n\t};\n};\n\nexport { validateEmail, validateNumber, validateString, validateDomains, validateDomain };\n"
  },
  {
    "path": "frontend/src/notifications/Msg.module.css",
    "content": ".toaster {\n\tpadding: 0;\n\tbackground: transparent !important;\n\tbox-shadow: none !important;\n\tborder: none !important;\n\n\t&.toast {\n\t\tborder-radius: 0;\n\t\tbox-shadow: none;\n\t\tfont-size: 14px;\n\t\tpadding: 16px 24px;\n\t\tbackground: transparent;\n\t}\n}\n"
  },
  {
    "path": "frontend/src/notifications/Msg.tsx",
    "content": "import { IconCheck, IconExclamationCircle } from \"@tabler/icons-react\";\nimport cn from \"classnames\";\nimport type { ReactNode } from \"react\";\n\nfunction Msg({ data }: any) {\n\tconst cns = cn(\"toast\", \"show\", data.type || null);\n\n\tlet icon: ReactNode = null;\n\tswitch (data.type) {\n\t\tcase \"success\":\n\t\t\ticon = <IconCheck className=\"text-green mr-1\" size={16} />;\n\t\t\tbreak;\n\t\tcase \"error\":\n\t\t\ticon = <IconExclamationCircle className=\"text-red mr-1\" size={16} />;\n\t\t\tbreak;\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cns}\n\t\t\trole=\"alert\"\n\t\t\taria-live=\"assertive\"\n\t\t\taria-atomic=\"true\"\n\t\t\tdata-bs-autohide=\"false\"\n\t\t\tdata-bs-toggle=\"toast\"\n\t\t>\n\t\t\t{data.title && (\n\t\t\t\t<div className=\"toast-header\">\n\t\t\t\t\t{icon} {data.title}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t<div className=\"toast-body\">{data.message}</div>\n\t\t</div>\n\t);\n}\nexport { Msg };\n"
  },
  {
    "path": "frontend/src/notifications/helpers.tsx",
    "content": "import { toast } from \"react-toastify\";\nimport { intl } from \"src/locale\";\nimport { Msg } from \"./Msg\";\nimport styles from \"./Msg.module.css\";\n\nconst showSuccess = (message: string) => {\n\ttoast(Msg, {\n\t\tclassName: styles.toaster,\n\t\tdata: {\n\t\t\ttype: \"success\",\n\t\t\ttitle: intl.formatMessage({ id: \"notification.success\" }),\n\t\t\tmessage,\n\t\t},\n\t});\n};\n\nconst showError = (message: string) => {\n\ttoast(<Msg />, {\n\t\tdata: {\n\t\t\ttype: \"error\",\n\t\t\ttitle: intl.formatMessage({ id: \"notification.error\" }),\n\t\t\tmessage,\n\t\t},\n\t});\n};\n\nconst showObjectSuccess = (obj: string, action: string) => {\n\tshowSuccess(\n\t\tintl.formatMessage(\n\t\t\t{\n\t\t\t\tid: `notification.object-${action}`,\n\t\t\t},\n\t\t\t{ object: intl.formatMessage({ id: obj }) },\n\t\t),\n\t);\n};\n\nexport { showSuccess, showError, showObjectSuccess };\n"
  },
  {
    "path": "frontend/src/notifications/index.ts",
    "content": "export * from \"./helpers\";\n"
  },
  {
    "path": "frontend/src/pages/Access/Table.tsx",
    "content": "import { IconDotsVertical, IconEdit, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { AccessList } from \"src/api/backend\";\nimport { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { ACCESS_LISTS, MANAGE } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: AccessList[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonEdit?: (id: number) => void;\n\tonDelete?: (id: number) => void;\n\tonNew?: () => void;\n}\nexport default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onNew }: Props) {\n\tconst columnHelper = createColumnHelper<AccessList>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"name\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.name\" }),\n\t\t\t\tcell: (info: any) => (\n\t\t\t\t\t<ValueWithDateFormatter value={info.getValue().name} createdOn={info.getValue().createdOn} />\n\t\t\t\t),\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.items, {\n\t\t\t\tid: \"items\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.authorization\" }),\n\t\t\t\tcell: (info: any) => <T id=\"access-list.auth-count\" data={{ count: info.getValue().length }} />,\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.clients, {\n\t\t\t\tid: \"clients\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.access\" }),\n\t\t\t\tcell: (info: any) => <T id=\"access-list.access-count\" data={{ count: info.getValue().length }} />,\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.satisfyAny, {\n\t\t\t\tid: \"satisfyAny\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.satisfy\" }),\n\t\t\t\tcell: (info: any) => <T id={info.getValue() ? \"column.satisfy-any\" : \"column.satisfy-all\"} />,\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.proxyHostCount, {\n\t\t\t\tid: \"proxyHostCount\",\n\t\t\t\theader: intl.formatMessage({ id: \"proxy-hosts\" }),\n\t\t\t\tcell: (info: any) => <T id=\"proxy-hosts.count\" data={{ count: info.getValue() }} />,\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"access-list\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEdit?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onEdit, onDelete],\n\t);\n\n\tconst tableInstance = useReactTable<AccessList>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"access-list\"\n\t\t\t\t\tobjects=\"access-lists\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNew}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"cyan\"\n\t\t\t\t\tpermissionSection={ACCESS_LISTS}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Access/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteAccessList } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useAccessLists } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showAccessListModal, showDeleteConfirmModal, showHelpModal } from \"src/modals\";\nimport { ACCESS_LISTS, MANAGE } from \"src/modules/Permissions\";\nimport { showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useAccessLists([\"owner\", \"items\", \"clients\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteAccessList(id);\n\t\tshowObjectSuccess(\"access-list\", \"deleted\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter((item) => {\n\t\t\treturn item.name.toLowerCase().includes(search);\n\t\t});\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-cyan\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"access-lists\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"AccessLists\", \"cyan\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn-cyan\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => showAccessListModal(\"new\")}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"access-list\" }} />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tisFiltered={!!filtered}\n\t\t\t\t\tonEdit={(id: number) => showAccessListModal(id)}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"access-list\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"access-lists\"], [\"access-list\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"access-list\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonNew={() => showAccessListModal(\"new\")}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Access/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { ACCESS_LISTS, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst Access = () => {\n\treturn (\n\t\t<HasPermission section={ACCESS_LISTS} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default Access;\n"
  },
  {
    "path": "frontend/src/pages/AuditLog/Table.tsx",
    "content": "import { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { AuditLog } from \"src/api/backend\";\nimport { EventFormatter, GravatarFormatter } from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\n\ninterface Props {\n\tdata: AuditLog[];\n\tisFetching?: boolean;\n\tonSelectItem?: (id: number) => void;\n}\nexport default function Table({ data, isFetching, onSelectItem }: Props) {\n\tconst columnHelper = createColumnHelper<AuditLog>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: AuditLog) => row.user, {\n\t\t\t\tid: \"user.avatar\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: AuditLog) => row, {\n\t\t\t\tid: \"objectType\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.event\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <EventFormatter row={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"btn btn-action btn-sm px-1\"\n\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\tonSelectItem?.(info.row.original.id);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<T id=\"action.view-details\" />\n\t\t\t\t\t\t</button>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onSelectItem],\n\t);\n\n\tconst tableInstance = useReactTable<AuditLog>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn <TableLayout tableInstance={tableInstance} />;\n}\n"
  },
  {
    "path": "frontend/src/pages/AuditLog/TableWrapper.tsx",
    "content": "import Alert from \"react-bootstrap/Alert\";\nimport { LoadingPage } from \"src/components\";\nimport { useAuditLogs } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showEventDetailsModal } from \"src/modals\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst { isFetching, isLoading, isError, error, data } = useAuditLogs([\"user\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-purple\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"auditlogs\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table data={data ?? []} isFetching={isFetching} onSelectItem={showEventDetailsModal} />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/AuditLog/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { ADMIN, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst AuditLog = () => {\n\treturn (\n\t\t<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default AuditLog;\n"
  },
  {
    "path": "frontend/src/pages/Certificates/Table.tsx",
    "content": "import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { Certificate } from \"src/api/backend\";\nimport {\n\tCertificateInUseFormatter,\n\tDateFormatter,\n\tDomainsFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tHasPermission,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from \"src/modals\";\nimport { CERTIFICATES, MANAGE } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: Certificate[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonDelete?: (id: number) => void;\n\tonRenew?: (id: number) => void;\n\tonDownload?: (id: number) => void;\n}\nexport default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) {\n\tconst columnHelper = createColumnHelper<Certificate>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"domainNames\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.name\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<DomainsFormatter\n\t\t\t\t\t\t\tdomains={value.domainNames}\n\t\t\t\t\t\t\tcreatedOn={value.createdOn}\n\t\t\t\t\t\t\tniceName={value.niceName}\n\t\t\t\t\t\t\tprovider={value.provider || \"\"}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"provider\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.provider\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst r = info.getValue();\n\t\t\t\t\tif (r.provider === \"letsencrypt\") {\n\t\t\t\t\t\tif (r.meta?.dnsChallenge && r.meta?.dnsProvider) {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<T id=\"lets-encrypt\" /> &ndash; {r.meta?.dnsProvider}\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn <T id=\"lets-encrypt\" />;\n\t\t\t\t\t}\n\t\t\t\t\tif (r.provider === \"other\") {\n\t\t\t\t\t\treturn <T id=\"certificates.custom\" />;\n\t\t\t\t\t}\n\t\t\t\t\treturn <T id={r.provider} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.expiresOn, {\n\t\t\t\tid: \"expiresOn\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.expires\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <DateFormatter value={info.getValue()} highlightPast />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"proxyHosts\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst r = info.getValue();\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<CertificateInUseFormatter\n\t\t\t\t\t\t\tproxyHosts={r.proxyHosts}\n\t\t\t\t\t\t\tredirectionHosts={r.redirectionHosts}\n\t\t\t\t\t\t\tdeadHosts={r.deadHosts}\n\t\t\t\t\t\t\tstreams={r.streams}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"certificate\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonRenew?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconRefresh size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.renew\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDownload?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconDownload size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.download\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onDelete, onRenew, onDownload],\n\t);\n\n\tconst tableInstance = useReactTable<Certificate>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\tconst customAddBtn = (\n\t\t<div className=\"dropdown\">\n\t\t\t<button type=\"button\" className=\"btn dropdown-toggle btn-pink my-3\" data-bs-toggle=\"dropdown\">\n\t\t\t\t<T id=\"object.add\" tData={{ object: \"certificate\" }} />\n\t\t\t</button>\n\t\t\t<div className=\"dropdown-menu\">\n\t\t\t\t<a\n\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\thref=\"#\"\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tshowHTTPCertificateModal();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<T id=\"lets-encrypt-via-http\" />\n\t\t\t\t</a>\n\t\t\t\t<a\n\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\thref=\"#\"\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tshowDNSCertificateModal();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<T id=\"lets-encrypt-via-dns\" />\n\t\t\t\t</a>\n\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t<a\n\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\thref=\"#\"\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tshowCustomCertificateModal();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<T id=\"certificates.custom\" />\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t</div>\n\t);\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"certificate\"\n\t\t\t\t\tobjects=\"certificates\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"pink\"\n\t\t\t\t\tcustomAddBtn={customAddBtn}\n\t\t\t\t\tpermissionSection={CERTIFICATES}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Certificates/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteCertificate, downloadCertificate } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useCertificates } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport {\n\tshowCustomCertificateModal,\n\tshowDeleteConfirmModal,\n\tshowDNSCertificateModal,\n\tshowHelpModal,\n\tshowHTTPCertificateModal,\n\tshowRenewCertificateModal,\n} from \"src/modals\";\nimport { CERTIFICATES, MANAGE } from \"src/modules/Permissions\";\nimport { showError, showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useCertificates([\n\t\t\"owner\",\n\t\t\"dead_hosts\",\n\t\t\"proxy_hosts\",\n\t\t\"redirection_hosts\",\n\t\t\"streams\",\n\t]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteCertificate(id);\n\t\tshowObjectSuccess(\"certificate\", \"deleted\");\n\t};\n\n\tconst handleDownload = async (id: number) => {\n\t\ttry {\n\t\t\tawait downloadCertificate(id);\n\t\t} catch (err: any) {\n\t\t\tshowError(err.message);\n\t\t}\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter(\n\t\t\t(item) =>\n\t\t\t\titem.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||\n\t\t\t\titem.niceName.toLowerCase().includes(search),\n\t\t);\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-pink\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"certificates\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"Certificates\", \"pink\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<div className=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn btn-sm dropdown-toggle btn-pink mt-1\"\n\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"certificate\" }} />\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"dropdown-menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tshowHTTPCertificateModal();\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"lets-encrypt-via-http\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tshowDNSCertificateModal();\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"lets-encrypt-via-dns\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tshowCustomCertificateModal();\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"certificates.custom\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFiltered={!!search}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tonRenew={showRenewCertificateModal}\n\t\t\t\t\tonDownload={handleDownload}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"certificate\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"certificates\"], [\"certificate\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"certificate\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Certificates/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { CERTIFICATES, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst Certificates = () => {\n\treturn (\n\t\t<HasPermission section={CERTIFICATES} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default Certificates;\n"
  },
  {
    "path": "frontend/src/pages/Dashboard/index.tsx",
    "content": "import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from \"@tabler/icons-react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { HasPermission } from \"src/components\";\nimport { useHostReport } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from \"src/modules/Permissions\";\n\nconst Dashboard = () => {\n\tconst { data: hostReport } = useHostReport();\n\tconst navigate = useNavigate();\n\n\treturn (\n\t\t<div>\n\t\t\t<h2>\n\t\t\t\t<T id=\"dashboard\" />\n\t\t\t</h2>\n\t\t\t<div className=\"row row-deck row-cards\">\n\t\t\t\t<div className=\"col-12 my-4\">\n\t\t\t\t\t<div className=\"row row-cards\">\n\t\t\t\t\t\t<HasPermission section={PROXY_HOSTS} permission={VIEW} hideError>\n\t\t\t\t\t\t\t<div className=\"col-sm-6 col-lg-3\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"/nginx/proxy\"\n\t\t\t\t\t\t\t\t\tclassName=\"card card-sm card-link card-link-pop\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tnavigate(\"/nginx/proxy\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"row align-items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"bg-green text-white avatar\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconBolt />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"font-weight-medium\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"proxy-hosts.count\" data={{ count: hostReport?.proxy }} />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} hideError>\n\t\t\t\t\t\t\t<div className=\"col-sm-6 col-lg-3\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"/nginx/redirection\"\n\t\t\t\t\t\t\t\t\tclassName=\"card card-sm card-link card-link-pop\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tnavigate(\"/nginx/redirection\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"row align-items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"bg-yellow text-white avatar\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconArrowsCross />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"redirection-hosts.count\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata={{ count: hostReport?.redirection }}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t<HasPermission section={STREAMS} permission={VIEW} hideError>\n\t\t\t\t\t\t\t<div className=\"col-sm-6 col-lg-3\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"/nginx/stream\"\n\t\t\t\t\t\t\t\t\tclassName=\"card card-sm card-link card-link-pop\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tnavigate(\"/nginx/stream\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"row align-items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"bg-blue text-white avatar\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconDisc />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"streams.count\" data={{ count: hostReport?.stream }} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t<HasPermission section={DEAD_HOSTS} permission={VIEW} hideError>\n\t\t\t\t\t\t\t<div className=\"col-sm-6 col-lg-3\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"/nginx/404\"\n\t\t\t\t\t\t\t\t\tclassName=\"card card-sm card-link card-link-pop\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tnavigate(\"/nginx/404\");\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"row align-items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col-auto\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"bg-red text-white avatar\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<IconBoltOff />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"dead-hosts.count\" data={{ count: hostReport?.dead }} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default Dashboard;\n"
  },
  {
    "path": "frontend/src/pages/Login/index.module.css",
    "content": ".logo {\n\twidth: 200px;\n}\n"
  },
  {
    "path": "frontend/src/pages/Login/index.tsx",
    "content": "import { Field, Form, Formik } from \"formik\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { Button, LocalePicker, Page, ThemeSwitcher } from \"src/components\";\nimport { useAuthState } from \"src/context\";\nimport { useHealth } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport { validateEmail, validateString } from \"src/modules/Validations\";\nimport styles from \"./index.module.css\";\n\nfunction TwoFactorForm() {\n\tconst codeRef = useRef<HTMLInputElement>(null);\n\tconst [formErr, setFormErr] = useState(\"\");\n\tconst { verifyTwoFactor, cancelTwoFactor } = useAuthState();\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tsetFormErr(\"\");\n\t\ttry {\n\t\t\tawait verifyTwoFactor(values.code);\n\t\t} catch (err) {\n\t\t\tif (err instanceof Error) {\n\t\t\t\tsetFormErr(err.message);\n\t\t\t}\n\t\t}\n\t\tsetSubmitting(false);\n\t};\n\n\tuseEffect(() => {\n\t\tcodeRef.current?.focus();\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<h2 className=\"h2 text-center mb-4\">\n\t\t\t\t<T id=\"login.2fa-title\" />\n\t\t\t</h2>\n\t\t\t<p className=\"text-secondary text-center mb-4\">\n\t\t\t\t<T id=\"login.2fa-description\" />\n\t\t\t</p>\n\t\t\t{formErr !== \"\" && <Alert variant=\"danger\">{formErr}</Alert>}\n\t\t\t<Formik initialValues={{ code: \"\" }} onSubmit={onSubmit}>\n\t\t\t\t{({ isSubmitting }) => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t<Field name=\"code\" validate={validateString(6, 20)}>\n\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"login.2fa-code\" />\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\tref={codeRef}\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tinputMode=\"numeric\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"one-time-code\"\n\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\tmaxLength={20}\n\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.code && form.touched.code ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"login.2fa-code-placeholder\" })}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.code}</div>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"form-footer d-flex gap-2\">\n\t\t\t\t\t\t\t<Button type=\"button\" fullWidth onClick={cancelTwoFactor} disabled={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"cancel\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button type=\"submit\" fullWidth color=\"azure\" isLoading={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"login.2fa-verify\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</>\n\t);\n}\n\nfunction LoginForm() {\n\tconst emailRef = useRef<HTMLInputElement>(null);\n\tconst [formErr, setFormErr] = useState(\"\");\n\tconst { login } = useAuthState();\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tsetFormErr(\"\");\n\t\ttry {\n\t\t\tawait login(values.email, values.password);\n\t\t} catch (err) {\n\t\t\tif (err instanceof Error) {\n\t\t\t\tsetFormErr(err.message);\n\t\t\t}\n\t\t}\n\t\tsetSubmitting(false);\n\t};\n\n\tuseEffect(() => {\n\t\temailRef.current?.focus();\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<h2 className=\"h2 text-center mb-4\">\n\t\t\t\t<T id=\"login.title\" />\n\t\t\t</h2>\n\t\t\t{formErr !== \"\" && <Alert variant=\"danger\">{formErr}</Alert>}\n\t\t\t<Formik\n\t\t\t\tinitialValues={\n\t\t\t\t\t{\n\t\t\t\t\t\temail: \"\",\n\t\t\t\t\t\tpassword: \"\",\n\t\t\t\t\t} as any\n\t\t\t\t}\n\t\t\t\tonSubmit={onSubmit}\n\t\t\t>\n\t\t\t\t{({ isSubmitting }) => (\n\t\t\t\t\t<Form>\n\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t<Field name=\"email\" validate={validateEmail()}>\n\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"email-address\" />\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\tref={emailRef}\n\t\t\t\t\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.email && form.touched.email ? \" is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"email-address\" })}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.email}</div>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t\t<Field name=\"password\" validate={validateString(8, 255)}>\n\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\">\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"password\" />\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"current-password\"\n\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\tmaxLength={255}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.password && form.touched.password ? \" is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"password\" })}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">{form.errors.password}</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"form-footer\">\n\t\t\t\t\t\t\t<Button type=\"submit\" fullWidth color=\"azure\" isLoading={isSubmitting}>\n\t\t\t\t\t\t\t\t<T id=\"sign-in\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Form>\n\t\t\t\t)}\n\t\t\t</Formik>\n\t\t</>\n\t);\n}\n\nexport default function Login() {\n\tconst { twoFactorChallenge } = useAuthState();\n\tconst health = useHealth();\n\n\tconst getVersion = () => {\n\t\tif (!health.data) {\n\t\t\treturn \"\";\n\t\t}\n\t\tconst v = health.data.version;\n\t\treturn `v${v.major}.${v.minor}.${v.revision}`;\n\t};\n\n\treturn (\n\t\t<Page className=\"page page-center\">\n\t\t\t<div className=\"container container-tight py-4\">\n\t\t\t\t<div className=\"d-flex justify-content-between align-items-center mb-4 ps-4 pe-3\">\n\t\t\t\t\t<img\n\t\t\t\t\t\tclassName={styles.logo}\n\t\t\t\t\t\tsrc=\"/images/logo-text-horizontal-grey.png\"\n\t\t\t\t\t\talt=\"Nginx Proxy Manager\"\n\t\t\t\t\t/>\n\t\t\t\t\t<div className=\"d-flex align-items-center gap-1\">\n\t\t\t\t\t\t<LocalePicker />\n\t\t\t\t\t\t<ThemeSwitcher />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"card card-md\">\n\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t{twoFactorChallenge ? <TwoFactorForm /> : <LoginForm />}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"text-center text-secondary mt-3\">{getVersion()}</div>\n\t\t\t</div>\n\t\t</Page>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/DeadHosts/Table.tsx",
    "content": "import { IconDotsVertical, IconEdit, IconPower, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { DeadHost } from \"src/api/backend\";\nimport {\n\tCertificateFormatter,\n\tDomainsFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tHasPermission,\n\tTrueFalseFormatter,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { DEAD_HOSTS, MANAGE } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: DeadHost[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonEdit?: (id: number) => void;\n\tonDelete?: (id: number) => void;\n\tonDisableToggle?: (id: number, enabled: boolean) => void;\n\tonNew?: () => void;\n}\nexport default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {\n\tconst columnHelper = createColumnHelper<DeadHost>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"domainNames\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.source\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.certificate, {\n\t\t\t\tid: \"certificate\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.ssl\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <CertificateFormatter certificate={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.enabled, {\n\t\t\t\tid: \"enabled\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <TrueFalseFormatter value={info.getValue()} trueLabel=\"online\" falseLabel=\"offline\" />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"dead-host\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEdit?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDisableToggle?.(info.row.original.id, !info.row.original.enabled);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconPower size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id={info.row.original.enabled ? \"action.disable\" : \"action.enable\"} />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onDelete, onEdit, onDisableToggle],\n\t);\n\n\tconst tableInstance = useReactTable<DeadHost>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"dead-host\"\n\t\t\t\t\tobjects=\"dead-hosts\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNew}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"red\"\n\t\t\t\t\tpermissionSection={DEAD_HOSTS}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteDeadHost, toggleDeadHost } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useDeadHosts } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from \"src/modals\";\nimport { DEAD_HOSTS, MANAGE } from \"src/modules/Permissions\";\nimport { showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst queryClient = useQueryClient();\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useDeadHosts([\"owner\", \"certificate\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteDeadHost(id);\n\t\tshowObjectSuccess(\"dead-host\", \"deleted\");\n\t};\n\n\tconst handleDisableToggle = async (id: number, enabled: boolean) => {\n\t\tawait toggleDeadHost(id, enabled);\n\t\tqueryClient.invalidateQueries({ queryKey: [\"dead-hosts\"] });\n\t\tqueryClient.invalidateQueries({ queryKey: [\"dead-host\", id] });\n\t\tshowObjectSuccess(\"dead-host\", enabled ? \"enabled\" : \"disabled\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter((item) => {\n\t\t\treturn item.domainNames.some((domain: string) => domain.toLowerCase().includes(search));\n\t\t});\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-red\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"dead-hosts\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"DeadHosts\", \"red\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<Button size=\"sm\" className=\"btn-red\" onClick={() => showDeadHostModal(\"new\")}>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"dead-host\" }} />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFiltered={!!search}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tonEdit={(id: number) => showDeadHostModal(id)}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"dead-host\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"dead-hosts\"], [\"dead-host\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"dead-host\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonDisableToggle={handleDisableToggle}\n\t\t\t\t\tonNew={() => showDeadHostModal(\"new\")}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/DeadHosts/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { DEAD_HOSTS, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst DeadHosts = () => {\n\treturn (\n\t\t<HasPermission section={DEAD_HOSTS} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default DeadHosts;\n"
  },
  {
    "path": "frontend/src/pages/Nginx/ProxyHosts/Table.tsx",
    "content": "import { IconDotsVertical, IconEdit, IconPower, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { ProxyHost } from \"src/api/backend\";\nimport {\n\tAccessListFormatter,\n\tCertificateFormatter,\n\tDomainsFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tHasPermission,\n\tTrueFalseFormatter,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { MANAGE, PROXY_HOSTS } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: ProxyHost[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonEdit?: (id: number) => void;\n\tonDelete?: (id: number) => void;\n\tonDisableToggle?: (id: number, enabled: boolean) => void;\n\tonNew?: () => void;\n}\nexport default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {\n\tconst columnHelper = createColumnHelper<ProxyHost>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"domainNames\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.source\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"forwardHost\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.destination\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn `${value.forwardScheme}://${value.forwardHost}:${value.forwardPort}`;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.certificate, {\n\t\t\t\tid: \"certificate\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.ssl\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <CertificateFormatter certificate={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.accessList, {\n\t\t\t\tid: \"accessList\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.access\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <AccessListFormatter access={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.enabled, {\n\t\t\t\tid: \"enabled\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <TrueFalseFormatter value={info.getValue()} trueLabel=\"online\" falseLabel=\"offline\" />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"proxy-host\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEdit?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDisableToggle?.(info.row.original.id, !info.row.original.enabled);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconPower size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id={info.row.original.enabled ? \"action.disable\" : \"action.enable\"} />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onEdit, onDisableToggle, onDelete],\n\t);\n\n\tconst tableInstance = useReactTable<ProxyHost>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"proxy-host\"\n\t\t\t\t\tobjects=\"proxy-hosts\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNew}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"lime\"\n\t\t\t\t\tpermissionSection={PROXY_HOSTS}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteProxyHost, toggleProxyHost } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useProxyHosts } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from \"src/modals\";\nimport { MANAGE, PROXY_HOSTS } from \"src/modules/Permissions\";\nimport { showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst queryClient = useQueryClient();\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useProxyHosts([\"owner\", \"access_list\", \"certificate\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteProxyHost(id);\n\t\tshowObjectSuccess(\"proxy-host\", \"deleted\");\n\t};\n\n\tconst handleDisableToggle = async (id: number, enabled: boolean) => {\n\t\tawait toggleProxyHost(id, enabled);\n\t\tqueryClient.invalidateQueries({ queryKey: [\"proxy-hosts\"] });\n\t\tqueryClient.invalidateQueries({ queryKey: [\"proxy-host\", id] });\n\t\tshowObjectSuccess(\"proxy-host\", enabled ? \"enabled\" : \"disabled\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter(\n\t\t\t(item) =>\n\t\t\t\titem.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||\n\t\t\t\titem.forwardHost.toLowerCase().includes(search) ||\n\t\t\t\t`${item.forwardPort}`.includes(search),\n\t\t);\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-lime\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"proxy-hosts\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"ProxyHosts\", \"lime\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn-lime\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => showProxyHostModal(\"new\")}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"proxy-host\" }} />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFiltered={!!search}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tonEdit={(id: number) => showProxyHostModal(id)}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"proxy-host\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"proxy-hosts\"], [\"proxy-host\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"proxy-host\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonDisableToggle={handleDisableToggle}\n\t\t\t\t\tonNew={() => showProxyHostModal(\"new\")}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/ProxyHosts/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { PROXY_HOSTS, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst ProxyHosts = () => {\n\treturn (\n\t\t<HasPermission section={PROXY_HOSTS} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default ProxyHosts;\n"
  },
  {
    "path": "frontend/src/pages/Nginx/RedirectionHosts/Table.tsx",
    "content": "import { IconDotsVertical, IconEdit, IconPower, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { RedirectionHost } from \"src/api/backend\";\nimport {\n\tCertificateFormatter,\n\tDomainsFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tHasPermission,\n\tTrueFalseFormatter,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { MANAGE, REDIRECTION_HOSTS } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: RedirectionHost[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonEdit?: (id: number) => void;\n\tonDelete?: (id: number) => void;\n\tonDisableToggle?: (id: number, enabled: boolean) => void;\n\tonNew?: () => void;\n}\nexport default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {\n\tconst columnHelper = createColumnHelper<RedirectionHost>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"domainNames\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.source\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <DomainsFormatter domains={value.domainNames} createdOn={value.createdOn} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.forwardHttpCode, {\n\t\t\t\tid: \"forwardHttpCode\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.http-code\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn info.getValue();\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.forwardScheme, {\n\t\t\t\tid: \"forwardScheme\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.scheme\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn info.getValue().toUpperCase();\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.forwardDomainName, {\n\t\t\t\tid: \"forwardDomainName\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.destination\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn info.getValue();\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.certificate, {\n\t\t\t\tid: \"certificate\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.ssl\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <CertificateFormatter certificate={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.enabled, {\n\t\t\t\tid: \"enabled\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <TrueFalseFormatter value={info.getValue()} trueLabel=\"online\" falseLabel=\"offline\" />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"redirection-host\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEdit?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDisableToggle?.(info.row.original.id, !info.row.original.enabled);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconPower size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id={info.row.original.enabled ? \"action.disable\" : \"action.enable\"} />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onEdit, onDisableToggle, onDelete],\n\t);\n\n\tconst tableInstance = useReactTable<RedirectionHost>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"redirection-host\"\n\t\t\t\t\tobjects=\"redirection-hosts\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNew}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"yellow\"\n\t\t\t\t\tpermissionSection={REDIRECTION_HOSTS}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteRedirectionHost, toggleRedirectionHost } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useRedirectionHosts } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from \"src/modals\";\nimport { MANAGE, REDIRECTION_HOSTS } from \"src/modules/Permissions\";\nimport { showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst queryClient = useQueryClient();\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useRedirectionHosts([\"owner\", \"certificate\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteRedirectionHost(id);\n\t\tshowObjectSuccess(\"redirection-host\", \"deleted\");\n\t};\n\n\tconst handleDisableToggle = async (id: number, enabled: boolean) => {\n\t\tawait toggleRedirectionHost(id, enabled);\n\t\tqueryClient.invalidateQueries({ queryKey: [\"redirection-hosts\"] });\n\t\tqueryClient.invalidateQueries({ queryKey: [\"redirection-host\", id] });\n\t\tshowObjectSuccess(\"redirection-host\", enabled ? \"enabled\" : \"disabled\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter((item) => {\n\t\t\treturn (\n\t\t\t\titem.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||\n\t\t\t\titem.forwardDomainName.toLowerCase().includes(search)\n\t\t\t);\n\t\t});\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-yellow\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"redirection-hosts\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"RedirectionHosts\", \"yellow\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"btn-yellow\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => showRedirectionHostModal(\"new\")}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"redirection-host\" }} />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFiltered={!!search}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tonEdit={(id: number) => showRedirectionHostModal(id)}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"redirection-host\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"redirection-hosts\"], [\"redirection-host\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"redirection-host\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonDisableToggle={handleDisableToggle}\n\t\t\t\t\tonNew={() => showRedirectionHostModal(\"new\")}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/RedirectionHosts/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { REDIRECTION_HOSTS, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst RedirectionHosts = () => {\n\treturn (\n\t\t<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default RedirectionHosts;\n"
  },
  {
    "path": "frontend/src/pages/Nginx/Streams/Table.tsx",
    "content": "import { IconDotsVertical, IconEdit, IconPower, IconTrash } from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { Stream } from \"src/api/backend\";\nimport {\n\tCertificateFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tHasPermission,\n\tTrueFalseFormatter,\n\tValueWithDateFormatter,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\nimport { MANAGE, STREAMS } from \"src/modules/Permissions\";\n\ninterface Props {\n\tdata: Stream[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tonEdit?: (id: number) => void;\n\tonDelete?: (id: number) => void;\n\tonDisableToggle?: (id: number, enabled: boolean) => void;\n\tonNew?: () => void;\n}\nexport default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onDisableToggle, onNew }: Props) {\n\tconst columnHelper = createColumnHelper<Stream>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row.owner, {\n\t\t\t\tid: \"owner\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value ? value.avatar : \"\"} name={value ? value.name : \"\"} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"incomingPort\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.incoming-port\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <ValueWithDateFormatter value={value.incomingPort} createdOn={value.createdOn} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"forwardHttpCode\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.destination\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn `${value.forwardingHost}:${value.forwardingPort}`;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"tcpForwarding\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.protocol\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{value.tcpForwarding ? (\n\t\t\t\t\t\t\t\t<span className=\"badge badge-lg domain-name\">\n\t\t\t\t\t\t\t\t\t<T id=\"streams.tcp\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t{value.udpForwarding ? (\n\t\t\t\t\t\t\t\t<span className=\"badge badge-lg domain-name\">\n\t\t\t\t\t\t\t\t\t<T id=\"streams.udp\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.certificate, {\n\t\t\t\tid: \"certificate\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.ssl\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <CertificateFormatter certificate={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.enabled, {\n\t\t\t\tid: \"enabled\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <TrueFalseFormatter value={info.getValue()} trueLabel=\"online\" falseLabel=\"offline\" />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"stream\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEdit?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t<HasPermission section={STREAMS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDisableToggle?.(info.row.original.id, !info.row.original.enabled);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconPower size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.disable\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\tonDelete?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[columnHelper, onEdit, onDisableToggle, onDelete],\n\t);\n\n\tconst tableInstance = useReactTable<Stream>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"stream\"\n\t\t\t\t\tobjects=\"streams\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNew}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"blue\"\n\t\t\t\t\tpermissionSection={STREAMS}\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/Streams/TableWrapper.tsx",
    "content": "import { IconHelp, IconSearch } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteStream, toggleStream } from \"src/api/backend\";\nimport { Button, HasPermission, LoadingPage } from \"src/components\";\nimport { useStreams } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showDeleteConfirmModal, showHelpModal, showStreamModal } from \"src/modals\";\nimport { MANAGE, STREAMS } from \"src/modules/Permissions\";\nimport { showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst queryClient = useQueryClient();\n\tconst [search, setSearch] = useState(\"\");\n\tconst [_deleteId, _setDeleteIdd] = useState(0);\n\tconst { isFetching, isLoading, isError, error, data } = useStreams([\"owner\", \"certificate\"]);\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteStream(id);\n\t\tshowObjectSuccess(\"stream\", \"deleted\");\n\t};\n\n\tconst handleDisableToggle = async (id: number, enabled: boolean) => {\n\t\tawait toggleStream(id, enabled);\n\t\tqueryClient.invalidateQueries({ queryKey: [\"streams\"] });\n\t\tqueryClient.invalidateQueries({ queryKey: [\"stream\", id] });\n\t\tshowObjectSuccess(\"stream\", enabled ? \"enabled\" : \"disabled\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter((item) => {\n\t\t\treturn (\n\t\t\t\t`${item.incomingPort}`.includes(search) ||\n\t\t\t\t`${item.forwardingPort}`.includes(search) ||\n\t\t\t\titem.forwardingHost.includes(search)\n\t\t\t);\n\t\t});\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-blue\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"streams\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<Button size=\"sm\" onClick={() => showHelpModal(\"Streams\", \"blue\")}>\n\t\t\t\t\t\t\t\t\t<IconHelp size={20} />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<HasPermission section={STREAMS} permission={MANAGE} hideError>\n\t\t\t\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t\t\t\t<Button size=\"sm\" className=\"btn-blue\" onClick={() => showStreamModal(\"new\")}>\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"stream\" }} />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</HasPermission>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tisFiltered={!!filtered}\n\t\t\t\t\tonEdit={(id: number) => showStreamModal(id)}\n\t\t\t\t\tonDelete={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"stream\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"streams\"], [\"stream\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"stream\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonDisableToggle={handleDisableToggle}\n\t\t\t\t\tonNew={() => showStreamModal(\"new\")}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Nginx/Streams/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { STREAMS, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst Streams = () => {\n\treturn (\n\t\t<HasPermission section={STREAMS} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default Streams;\n"
  },
  {
    "path": "frontend/src/pages/Settings/DefaultSite.tsx",
    "content": "import CodeEditor from \"@uiw/react-textarea-code-editor\";\nimport { Field, Form, Formik } from \"formik\";\nimport { type ReactNode, useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport { Button, Loading } from \"src/components\";\nimport { useSetSetting, useSetting } from \"src/hooks\";\nimport { intl, T } from \"src/locale\";\nimport { validateString } from \"src/modules/Validations\";\nimport { showObjectSuccess } from \"src/notifications\";\n\nexport default function DefaultSite() {\n\tconst { data, isLoading, error } = useSetting(\"default-site\");\n\tconst { mutate: setSetting } = useSetSetting();\n\tconst [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\n\tconst onSubmit = async (values: any, { setSubmitting }: any) => {\n\t\tif (isSubmitting) return;\n\t\tsetIsSubmitting(true);\n\t\tsetErrorMsg(null);\n\n\t\tconst payload = {\n\t\t\tid: \"default-site\",\n\t\t\tvalue: values.value,\n\t\t\tmeta: {\n\t\t\t\tredirect: values.redirect,\n\t\t\t\thtml: values.html,\n\t\t\t},\n\t\t};\n\n\t\tsetSetting(payload, {\n\t\t\tonError: (err: any) => setErrorMsg(<T id={err.message} />),\n\t\t\tonSuccess: () => {\n\t\t\t\tshowObjectSuccess(\"setting\", \"saved\");\n\t\t\t},\n\t\t\tonSettled: () => {\n\t\t\t\tsetIsSubmitting(false);\n\t\t\t\tsetSubmitting(false);\n\t\t\t},\n\t\t});\n\t};\n\n\tif (!isLoading && error) {\n\t\treturn (\n\t\t\t<div className=\"card-body\">\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<Alert variant=\"danger\" show>\n\t\t\t\t\t\t{error.message}\n\t\t\t\t\t</Alert>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (isLoading) {\n\t\treturn (\n\t\t\t<div className=\"card-body\">\n\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t<Loading noLogo />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<Formik\n\t\t\tinitialValues={\n\t\t\t\t{\n\t\t\t\t\tvalue: data?.value || \"congratulations\",\n\t\t\t\t\tredirect: data?.meta?.redirect || \"\",\n\t\t\t\t\thtml: data?.meta?.html || \"\",\n\t\t\t\t} as any\n\t\t\t}\n\t\t\tonSubmit={onSubmit}\n\t\t>\n\t\t\t{({ values }) => (\n\t\t\t\t<Form>\n\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t<Field name=\"value\">\n\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"setting-host-unknown\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.description\" />\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup form-selectgroup-boxes d-flex flex-column\">\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-selectgroup-item flex-fill\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"congratulations\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-selectgroup-input\"\n\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"congratulations\"}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => form.setFieldValue(field.name, e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup-label d-flex align-items-center p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"me-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-selectgroup-check\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.congratulations\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-selectgroup-item flex-fill\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"404\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-selectgroup-input\"\n\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"404\"}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => form.setFieldValue(field.name, e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup-label d-flex align-items-center p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"me-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-selectgroup-check\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.404\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-selectgroup-item flex-fill\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"444\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-selectgroup-input\"\n\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"444\"}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => form.setFieldValue(field.name, e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup-label d-flex align-items-center p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"me-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-selectgroup-check\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.444\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-selectgroup-item flex-fill\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"redirect\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-selectgroup-input\"\n\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"redirect\"}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => form.setFieldValue(field.name, e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup-label d-flex align-items-center p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"me-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-selectgroup-check\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.redirect\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-selectgroup-item flex-fill\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\t\t\tname={field.name}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue=\"html\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-selectgroup-input\"\n\t\t\t\t\t\t\t\t\t\t\t\tchecked={field.value === \"html\"}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => form.setFieldValue(field.name, e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-selectgroup-label d-flex align-items-center p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"me-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"form-selectgroup-check\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.html\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t{values.value === \"redirect\" && (\n\t\t\t\t\t\t\t<Field name=\"redirect\" validate={validateString(1, 255)}>\n\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t<div className=\"mt-5 mb-3\">\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"setting-host-unknown\">\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.redirect\" />\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\tid=\"redirect\"\n\t\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"https://\"\n\t\t\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control\"\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t{form.errors.redirect ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.redirect && form.touched.redirect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.redirect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{values.value === \"html\" && (\n\t\t\t\t\t\t\t<Field name=\"html\" validate={validateString(1)}>\n\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t<div className=\"mt-5 mb-3\">\n\t\t\t\t\t\t\t\t\t\t<label className=\"form-label\" htmlFor=\"setting-host-unknown\">\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site.html\" />\n\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\t\t\t\t\t\t// Believe it or not, 'html' sucks yet 'php' renders the html\n\t\t\t\t\t\t\t\t\t\t\t\t// content much nicer.\n\t\t\t\t\t\t\t\t\t\t\t\tlanguage=\"php\"\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: \"settings.default-site.html.placeholder\",\n\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\tpadding={15}\n\t\t\t\t\t\t\t\t\t\t\t\tdata-color-mode=\"dark\"\n\t\t\t\t\t\t\t\t\t\t\t\tminHeight={300}\n\t\t\t\t\t\t\t\t\t\t\t\tindentWidth={2}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tfontFamily:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: \"0.3rem\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tminHeight: \"300px\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: \"var(--tblr-bg-surface-dark)\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t{form.errors.html ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.html && form.touched.html ? form.errors.html : null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"card-footer bg-transparent mt-auto\">\n\t\t\t\t\t\t<div className=\"btn-list justify-content-end\">\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\tclassName=\"ms-auto bg-teal\"\n\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</Form>\n\t\t\t)}\n\t\t</Formik>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Settings/Layout.tsx",
    "content": "import { T } from \"src/locale\";\nimport DefaultSite from \"./DefaultSite\";\n\nexport default function Layout() {\n\t// Taken from https://preview.tabler.io/settings.html\n\t// Refer to that when updating this content\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-teal\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t<T id=\"settings\" />\n\t\t\t\t\t\t</h2>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"row g-0\">\n\t\t\t\t\t<div className=\"col-12 col-md-3 border-end\">\n\t\t\t\t\t\t<div className=\"card-body mt-0 pt-0\">\n\t\t\t\t\t\t\t<div className=\"list-group list-group-transparent\">\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tclassName=\"list-group-item list-group-item-action d-flex align-items-center active\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => e.preventDefault()}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<T id=\"settings.default-site\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"col-12 col-md-9 d-flex flex-column\">\n\t\t\t\t\t\t<DefaultSite />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Settings/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { ADMIN, VIEW } from \"src/modules/Permissions\";\nimport Layout from \"./Layout\";\n\nconst Settings = () => {\n\treturn (\n\t\t<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<Layout />\n\t\t</HasPermission>\n\t);\n};\n\nexport default Settings;\n"
  },
  {
    "path": "frontend/src/pages/Setup/index.module.css",
    "content": ".logo {\n\twidth: 200px;\n}\n\n.helperBtns {\n\tposition: absolute;\n\ttop: 10px;\n\tright: 10px;\n\tz-index: 1000;\n}\n"
  },
  {
    "path": "frontend/src/pages/Setup/index.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport cn from \"classnames\";\nimport { Field, Form, Formik } from \"formik\";\nimport { useState } from \"react\";\nimport { Alert } from \"react-bootstrap\";\nimport { createUser } from \"src/api/backend\";\nimport { Button, LocalePicker, Page, ThemeSwitcher } from \"src/components\";\nimport { useAuthState } from \"src/context\";\nimport { intl, T } from \"src/locale\";\nimport { validateEmail, validateString } from \"src/modules/Validations\";\nimport styles from \"./index.module.css\";\n\ninterface Payload {\n\tname: string;\n\temail: string;\n\tpassword: string;\n}\n\nexport default function Setup() {\n\tconst queryClient = useQueryClient();\n\tconst { login } = useAuthState();\n\tconst [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n\tconst onSubmit = async (values: Payload, { setSubmitting }: any) => {\n\t\tsetErrorMsg(null);\n\n\t\t// Set a nickname, which is the first word of the name\n\t\tconst nickname = values.name.split(\" \")[0];\n\n\t\tconst { password, ...payload } = {\n\t\t\t...values,\n\t\t\t...{\n\t\t\t\tnickname,\n\t\t\t\tauth: {\n\t\t\t\t\ttype: \"password\",\n\t\t\t\t\tsecret: values.password,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\n\t\ttry {\n\t\t\tconst user = await createUser(payload, true);\n\t\t\tif (user?.id) {\n\t\t\t\ttry {\n\t\t\t\t\tawait login(user.email, password);\n\t\t\t\t\t// Trigger a Health change\n\t\t\t\t\tawait queryClient.refetchQueries({ queryKey: [\"health\"] });\n\t\t\t\t\t// window.location.reload();\n\t\t\t\t} catch (err: any) {\n\t\t\t\t\tsetErrorMsg(err.message);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsetErrorMsg(\"cannot_create_user\");\n\t\t\t}\n\t\t} catch (err: any) {\n\t\t\tsetErrorMsg(err.message);\n\t\t}\n\t\tsetSubmitting(false);\n\t};\n\n\treturn (\n\t\t<Page className=\"page page-center\">\n\t\t\t<div className={cn(\"d-none\", \"d-md-flex\", styles.helperBtns)}>\n\t\t\t\t<LocalePicker />\n\t\t\t\t<ThemeSwitcher />\n\t\t\t</div>\n\t\t\t<div className=\"container container-tight py-4\">\n\t\t\t\t<div className=\"text-center mb-4\">\n\t\t\t\t\t<img\n\t\t\t\t\t\tclassName={styles.logo}\n\t\t\t\t\t\tsrc=\"/images/logo-text-horizontal-grey.png\"\n\t\t\t\t\t\talt=\"Nginx Proxy Manager\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"card card-md\">\n\t\t\t\t\t<Alert variant=\"danger\" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>\n\t\t\t\t\t\t{errorMsg}\n\t\t\t\t\t</Alert>\n\t\t\t\t\t<Formik\n\t\t\t\t\t\tinitialValues={\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\t\t\temail: \"\",\n\t\t\t\t\t\t\t\tpassword: \"\",\n\t\t\t\t\t\t\t} as any\n\t\t\t\t\t\t}\n\t\t\t\t\t\tonSubmit={onSubmit}\n\t\t\t\t\t>\n\t\t\t\t\t\t{({ isSubmitting }) => (\n\t\t\t\t\t\t\t<Form>\n\t\t\t\t\t\t\t\t<div className=\"card-body text-center py-4 p-sm-5\">\n\t\t\t\t\t\t\t\t\t<h1 className=\"mt-5\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"setup.title\" />\n\t\t\t\t\t\t\t\t\t</h1>\n\t\t\t\t\t\t\t\t\t<p className=\"text-secondary\">\n\t\t\t\t\t\t\t\t\t\t<T id=\"setup.preamble\" />\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<hr />\n\t\t\t\t\t\t\t\t<div className=\"card-body\">\n\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t<Field name=\"name\" validate={validateString(1, 50)}>\n\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"name\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.name && form.touched.name ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.full-name\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"name\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.full-name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.name && form.touched.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t<Field name=\"email\" validate={validateEmail()}>\n\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.email && form.touched.email ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"email-address\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"email\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"email-address\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.email ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.email && form.touched.email\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.email\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"mb-3\">\n\t\t\t\t\t\t\t\t\t\t<Field name=\"password\" validate={validateString(8, 100)}>\n\t\t\t\t\t\t\t\t\t\t\t{({ field, form }: any) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"form-floating mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`form-control ${form.errors.password && form.touched.password ? \"is-invalid\" : \"\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={intl.formatMessage({ id: \"user.new-password\" })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{...field}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<label htmlFor=\"password\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.new-password\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.password ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"invalid-feedback\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{form.errors.password && form.touched.password\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? form.errors.password\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"text-center my-3 mx-3\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\t\tactionType=\"primary\"\n\t\t\t\t\t\t\t\t\t\tdata-bs-dismiss=\"modal\"\n\t\t\t\t\t\t\t\t\t\tisLoading={isSubmitting}\n\t\t\t\t\t\t\t\t\t\tdisabled={isSubmitting}\n\t\t\t\t\t\t\t\t\t\tclassName=\"w-100\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<T id=\"save\" />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Formik>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Page>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Users/Table.tsx",
    "content": "import {\n\tIconDotsVertical,\n\tIconEdit,\n\tIconLock,\n\tIconLogin2,\n\tIconPower,\n\tIconShield,\n\tIconTrash,\n} from \"@tabler/icons-react\";\nimport { createColumnHelper, getCoreRowModel, useReactTable } from \"@tanstack/react-table\";\nimport { useMemo } from \"react\";\nimport type { User } from \"src/api/backend\";\nimport {\n\tEmailFormatter,\n\tEmptyData,\n\tGravatarFormatter,\n\tRolesFormatter,\n\tTrueFalseFormatter,\n\tValueWithDateFormatter,\n} from \"src/components\";\nimport { TableLayout } from \"src/components/Table/TableLayout\";\nimport { intl, T } from \"src/locale\";\n\ninterface Props {\n\tdata: User[];\n\tisFiltered?: boolean;\n\tisFetching?: boolean;\n\tcurrentUserId?: number;\n\tonEditUser?: (id: number) => void;\n\tonEditPermissions?: (id: number) => void;\n\tonSetPassword?: (id: number) => void;\n\tonDeleteUser?: (id: number) => void;\n\tonDisableToggle?: (id: number, enabled: boolean) => void;\n\tonNewUser?: () => void;\n\tonLoginAs?: (id: number) => void;\n}\nexport default function Table({\n\tdata,\n\tisFiltered,\n\tisFetching,\n\tcurrentUserId,\n\tonEditUser,\n\tonEditPermissions,\n\tonSetPassword,\n\tonDeleteUser,\n\tonDisableToggle,\n\tonNewUser,\n\tonLoginAs,\n}: Props) {\n\tconst columnHelper = createColumnHelper<User>();\n\tconst columns = useMemo(\n\t\t() => [\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"avatar\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\treturn <GravatarFormatter url={value.avatar} name={value.name} />;\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row, {\n\t\t\t\tid: \"name\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.name\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\tconst value = info.getValue();\n\t\t\t\t\t// Hack to reuse domains formatter\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<ValueWithDateFormatter\n\t\t\t\t\t\t\tvalue={value.name}\n\t\t\t\t\t\t\tcreatedOn={value.createdOn}\n\t\t\t\t\t\t\tdisabled={value.isDisabled}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.email, {\n\t\t\t\tid: \"email\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.email\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <EmailFormatter email={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.roles, {\n\t\t\t\tid: \"roles\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.roles\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <RolesFormatter roles={info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.accessor((row: any) => row.isDisabled, {\n\t\t\t\tid: \"isDisabled\",\n\t\t\t\theader: intl.formatMessage({ id: \"column.status\" }),\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn <TrueFalseFormatter value={!info.getValue()} />;\n\t\t\t\t},\n\t\t\t}),\n\t\t\tcolumnHelper.display({\n\t\t\t\tid: \"id\",\n\t\t\t\tcell: (info: any) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<span className=\"dropdown\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclassName=\"btn dropdown-toggle btn-action btn-sm px-1\"\n\t\t\t\t\t\t\t\tdata-bs-boundary=\"viewport\"\n\t\t\t\t\t\t\t\tdata-bs-toggle=\"dropdown\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<IconDotsVertical />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<div className=\"dropdown-menu dropdown-menu-end\">\n\t\t\t\t\t\t\t\t<span className=\"dropdown-header\">\n\t\t\t\t\t\t\t\t\t<T\n\t\t\t\t\t\t\t\t\t\tid=\"object.actions-title\"\n\t\t\t\t\t\t\t\t\t\ttData={{ object: \"user\" }}\n\t\t\t\t\t\t\t\t\t\tdata={{ id: info.row.original.id }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tonEditUser?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<IconEdit size={16} />\n\t\t\t\t\t\t\t\t\t<T id=\"action.edit\" />\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t{currentUserId !== info.row.original.id ? (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\tonEditPermissions?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<IconShield size={16} />\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"action.permissions\" />\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\tonSetPassword?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<IconLock size={16} />\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.set-password\" />\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\tonDisableToggle?.(info.row.original.id, info.row.original.isDisabled);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<IconPower size={16} />\n\t\t\t\t\t\t\t\t\t\t\t<T id={info.row.original.isDisabled ? \"action.enable\" : \"action.disable\"} />\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t{info.row.original.isDisabled ? (\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"dropdown-item text-muted\">\n\t\t\t\t\t\t\t\t\t\t\t\t<IconLogin2 size={16} />\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.login-as\" data={{ name: info.row.original.name }} />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\tonLoginAs?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<IconLogin2 size={16} />\n\t\t\t\t\t\t\t\t\t\t\t\t<T id=\"user.login-as\" data={{ name: info.row.original.name }} />\n\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t<div className=\"dropdown-divider\" />\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"dropdown-item\"\n\t\t\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\tonDeleteUser?.(info.row.original.id);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<IconTrash size={16} />\n\t\t\t\t\t\t\t\t\t\t\t<T id=\"action.delete\" />\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tclassName: \"text-end w-1\",\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t[\n\t\t\tcolumnHelper,\n\t\t\tcurrentUserId,\n\t\t\tonEditUser,\n\t\t\tonDisableToggle,\n\t\t\tonDeleteUser,\n\t\t\tonEditPermissions,\n\t\t\tonSetPassword,\n\t\t\tonLoginAs,\n\t\t],\n\t);\n\n\tconst tableInstance = useReactTable<User>({\n\t\tcolumns,\n\t\tdata,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\trowCount: data.length,\n\t\tmeta: {\n\t\t\tisFetching,\n\t\t},\n\t\tenableSortingRemoval: false,\n\t});\n\n\treturn (\n\t\t<TableLayout\n\t\t\ttableInstance={tableInstance}\n\t\t\temptyState={\n\t\t\t\t<EmptyData\n\t\t\t\t\tobject=\"user\"\n\t\t\t\t\tobjects=\"users\"\n\t\t\t\t\ttableInstance={tableInstance}\n\t\t\t\t\tonNew={onNewUser}\n\t\t\t\t\tisFiltered={isFiltered}\n\t\t\t\t\tcolor=\"orange\"\n\t\t\t\t/>\n\t\t\t}\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Users/TableWrapper.tsx",
    "content": "import { IconSearch } from \"@tabler/icons-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport Alert from \"react-bootstrap/Alert\";\nimport { deleteUser, toggleUser } from \"src/api/backend\";\nimport { Button, LoadingPage } from \"src/components\";\nimport { useAuthState } from \"src/context\";\nimport { useUser, useUsers } from \"src/hooks\";\nimport { T } from \"src/locale\";\nimport { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from \"src/modals\";\nimport { showError, showObjectSuccess } from \"src/notifications\";\nimport Table from \"./Table\";\n\nexport default function TableWrapper() {\n\tconst queryClient = useQueryClient();\n\tconst { loginAs } = useAuthState();\n\tconst [search, setSearch] = useState(\"\");\n\tconst { isFetching, isLoading, isError, error, data } = useUsers([\"permissions\"]);\n\tconst { data: currentUser } = useUser(\"me\");\n\n\tif (isLoading) {\n\t\treturn <LoadingPage />;\n\t}\n\n\tif (isError) {\n\t\treturn <Alert variant=\"danger\">{error?.message || \"Unknown error\"}</Alert>;\n\t}\n\n\tconst handleLoginAs = async (id: number) => {\n\t\ttry {\n\t\t\tawait loginAs(id);\n\t\t} catch (err) {\n\t\t\tif (err instanceof Error) {\n\t\t\t\tshowError(err.message);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst handleDelete = async (id: number) => {\n\t\tawait deleteUser(id);\n\t\tshowObjectSuccess(\"user\", \"deleted\");\n\t};\n\n\tconst handleDisableToggle = async (id: number, enabled: boolean) => {\n\t\tawait toggleUser(id, enabled);\n\t\tqueryClient.invalidateQueries({ queryKey: [\"users\"] });\n\t\tqueryClient.invalidateQueries({ queryKey: [\"user\", id] });\n\t\tshowObjectSuccess(\"user\", enabled ? \"enabled\" : \"disabled\");\n\t};\n\n\tlet filtered = null;\n\tif (search && data) {\n\t\tfiltered = data?.filter((item) => {\n\t\t\treturn (\n\t\t\t\titem.name.toLowerCase().includes(search) ||\n\t\t\t\titem.nickname.toLowerCase().includes(search) ||\n\t\t\t\titem.email.toLowerCase().includes(search)\n\t\t\t);\n\t\t});\n\t} else if (search !== \"\") {\n\t\t// this can happen if someone deletes the last item while searching\n\t\tsetSearch(\"\");\n\t}\n\n\treturn (\n\t\t<div className=\"card mt-4\">\n\t\t\t<div className=\"card-status-top bg-orange\" />\n\t\t\t<div className=\"card-table\">\n\t\t\t\t<div className=\"card-header\">\n\t\t\t\t\t<div className=\"row w-full\">\n\t\t\t\t\t\t<div className=\"col\">\n\t\t\t\t\t\t\t<h2 className=\"mt-1 mb-0\">\n\t\t\t\t\t\t\t\t<T id=\"users\" />\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{data?.length ? (\n\t\t\t\t\t\t\t<div className=\"col-md-auto col-sm-12\">\n\t\t\t\t\t\t\t\t<div className=\"ms-auto d-flex flex-wrap btn-list\">\n\t\t\t\t\t\t\t\t\t<div className=\"input-group input-group-flat w-auto\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"input-group-text input-group-text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<IconSearch size={16} />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"advanced-table-search\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"form-control form-control-sm\"\n\t\t\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tonChange={(e: any) => setSearch(e.target.value.toLowerCase().trim())}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<Button size=\"sm\" className=\"btn-orange\" onClick={() => showUserModal(\"new\")}>\n\t\t\t\t\t\t\t\t\t\t<T id=\"object.add\" tData={{ object: \"user\" }} />\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Table\n\t\t\t\t\tdata={filtered ?? data ?? []}\n\t\t\t\t\tisFiltered={!!search}\n\t\t\t\t\tisFetching={isFetching}\n\t\t\t\t\tcurrentUserId={currentUser?.id}\n\t\t\t\t\tonEditUser={(id: number) => showUserModal(id)}\n\t\t\t\t\tonEditPermissions={(id: number) => showPermissionsModal(id)}\n\t\t\t\t\tonSetPassword={(id: number) => showSetPasswordModal(id)}\n\t\t\t\t\tonDeleteUser={(id: number) =>\n\t\t\t\t\t\tshowDeleteConfirmModal({\n\t\t\t\t\t\t\ttitle: <T id=\"object.delete\" tData={{ object: \"user\" }} />,\n\t\t\t\t\t\t\tonConfirm: () => handleDelete(id),\n\t\t\t\t\t\t\tinvalidations: [[\"users\"], [\"user\", id]],\n\t\t\t\t\t\t\tchildren: <T id=\"object.delete.content\" tData={{ object: \"user\" }} />,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tonDisableToggle={handleDisableToggle}\n\t\t\t\t\tonNewUser={() => showUserModal(\"new\")}\n\t\t\t\t\tonLoginAs={handleLoginAs}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "frontend/src/pages/Users/index.tsx",
    "content": "import { HasPermission } from \"src/components\";\nimport { ADMIN, VIEW } from \"src/modules/Permissions\";\nimport TableWrapper from \"./TableWrapper\";\n\nconst Users = () => {\n\treturn (\n\t\t<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>\n\t\t\t<TableWrapper />\n\t\t</HasPermission>\n\t);\n};\n\nexport default Users;\n"
  },
  {
    "path": "frontend/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ES2020\",\n\t\t\"useDefineForClassFields\": true,\n\t\t\"lib\": [\n\t\t\t\"ES2020\",\n\t\t\t\"DOM\",\n\t\t\t\"DOM.Iterable\"\n\t\t],\n\t\t\"module\": \"ESNext\",\n\t\t\"skipLibCheck\": true,\n\t\t\"baseUrl\": \".\",\n\t\t/* Bundler mode */\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"noEmit\": true,\n\t\t\"jsx\": \"react-jsx\",\n\t\t/* Linting */\n\t\t\"strict\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"paths\": {\n\t\t\t\"src/*\": [\n\t\t\t\t\"./src/*\"\n\t\t\t],\n\t\t\t\"test/*\": [\n\t\t\t\t\"./test/*\"\n\t\t\t]\n\t\t}\n\t},\n\t\"include\": [\n\t\t\"src\",\n\t\t\"test\"\n\t],\n\t\"references\": [\n\t\t{\n\t\t\t\"path\": \"./tsconfig.node.json\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"composite\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowSyntheticDefaultImports\": true\n\t},\n\t\"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport checker from \"vite-plugin-checker\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport \"vitest/config\";\nimport { execFile } from \"node:child_process\";\n\nconst runLocaleScripts = () => {\n\texecFile(\"yarn\", [\"locale-compile\"], (error, stdout, _stderr) => {\n\t\tif (error) {\n\t\t\tthrow error;\n\t\t}\n\t\tconsole.log(stdout);\n\t\texecFile(\"yarn\", [\"locale-sort\"], (error, stdout, _stderr) => {\n\t\t\tif (error) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconsole.log(stdout);\n\t\t});\n\t});\n};\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n\tplugins: [\n\t\t{\n\t\t\tname: 'run-on-start',\n\t\t\tconfigureServer(_server) {\n\t\t\t\trunLocaleScripts();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"trigger-on-reload\",\n\t\t\tconfigureServer(server) {\n\t\t\t\tserver.watcher.on(\"change\", (file) => {\n\t\t\t\t\tif (file.includes(\"locale/src\")) {\n\t\t\t\t\t\tconsole.log(`File changed: ${file}, running locale scripts...`);\n\t\t\t\t\t\trunLocaleScripts();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t\treact(),\n\t\tchecker({\n\t\t\t// e.g. use TypeScript check\n\t\t\ttypescript: true,\n\t\t}),\n\t\ttsconfigPaths(),\n\t],\n\tserver: {\n\t\thost: true,\n\t\tport: 5173,\n\t\tstrictPort: true,\n\t\tallowedHosts: true,\n\t},\n\ttest: {\n\t\tenvironment: \"happy-dom\",\n\t\tsetupFiles: [\"./vitest-setup.js\"],\n\t},\n\tassetsInclude: [\"**/*.md\", \"**/*.png\", \"**/*.svg\"],\n});\n"
  },
  {
    "path": "frontend/vitest-setup.js",
    "content": "import \"@testing-library/jest-dom/vitest\";\n"
  },
  {
    "path": "scripts/.common.sh",
    "content": "#!/bin/bash\n\n# Colors\nBLUE='\\E[1;34m'\nCYAN='\\E[1;36m'\nGREEN='\\E[1;32m'\nRED='\\E[1;31m'\nRESET='\\E[0m'\nYELLOW='\\E[1;33m'\n\nexport BLUE CYAN GREEN RED RESET YELLOW\n\n# Docker Compose\nCOMPOSE_PROJECT_NAME=\"npm2dev\"\nCOMPOSE_FILE=\"docker/docker-compose.dev.yml\"\n\nexport COMPOSE_FILE COMPOSE_PROJECT_NAME\n\n# $1: container_name\nget_container_ip () {\n\tlocal container_name=$1\n\tlocal container\n\tlocal ip\n\tcontainer=$(docker compose ps --all -q \"${container_name}\" | tail -n1)\n\tip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \"$container\")\n\techo \"$ip\"\n}\n"
  },
  {
    "path": "scripts/buildx",
    "content": "#!/bin/bash\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\necho -e \"${BLUE}❯ ${CYAN}Building docker multiarch: ${YELLOW}${*}${RESET}\"\n\ncd \"${DIR}/..\" || exit 1\n\n# determine commit if not already set\nif [ \"$BUILD_COMMIT\" == \"\" ]; then\n\tBUILD_COMMIT=$(git log -n 1 --format=%h)\nfi\n\n# Buildx Builder\ndocker buildx create --name \"${BUILDX_NAME:-npm}\" || echo\ndocker buildx use \"${BUILDX_NAME:-npm}\"\n\ndocker buildx build \\\n\t--build-arg BUILD_VERSION=\"${BUILD_VERSION:-dev}\" \\\n\t--build-arg BUILD_COMMIT=\"${BUILD_COMMIT:-notset}\" \\\n\t--build-arg BUILD_DATE=\"$(date '+%Y-%m-%d %T %Z')\" \\\n\t--build-arg GOPROXY=\"${GOPROXY:-}\" \\\n\t--build-arg GOPRIVATE=\"${GOPRIVATE:-}\" \\\n\t--platform linux/amd64,linux/arm64 \\\n\t--progress plain \\\n\t--pull \\\n\t-f docker/Dockerfile \\\n\t$@ \\\n\t.\n\nrc=$?\ndocker buildx rm \"${BUILDX_NAME:-npm}\"\necho -e \"${BLUE}❯ ${GREEN}Multiarch build Complete${RESET}\"\nexit $rc\n"
  },
  {
    "path": "scripts/ci/frontend-build",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/../.common.sh\"\n\nDOCKER_IMAGE=nginxproxymanager/nginx-full:certbot-node\n\n# Ensure docker exists\nif hash docker 2>/dev/null; then\n\tdocker pull \"${DOCKER_IMAGE}\"\n\tcd \"${DIR}/../..\"\n\techo -e \"${BLUE}❯ ${CYAN}Building Frontend ...${RESET}\"\n\n\tdocker run --rm \\\n\t\t-e CI=true \\\n\t\t-e NODE_OPTIONS=--openssl-legacy-provider \\\n\t\t-v \"$(pwd)/frontend:/app/frontend\" \\\n\t\t-w /app/frontend \"${DOCKER_IMAGE}\" \\\n\t\tsh -c \"yarn install && yarn lint && yarn locale-compile && yarn vitest run --no-color && yarn build && chown -R $(id -u):$(id -g) /app/frontend\"\n\n\techo -e \"${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}\"\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/ci/fulltest-cypress",
    "content": "#!/bin/bash\nset -e\n\nSTACK=\"${1:-sqlite}\"\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# remember this is running in \"ci\" folder..\n\n# Some defaults for running this script outside of CI\nexport COMPOSE_PROJECT_NAME=\"${COMPOSE_PROJECT_NAME:-npm_local_fulltest}\"\nexport IMAGE=\"${IMAGE:-nginx-proxy-manager}\"\nexport BRANCH_LOWER=\"${BRANCH_LOWER:-unknown}\"\nexport BUILD_NUMBER=\"${BUILD_NUMBER:-0000}\"\n\nif [ \"${COMPOSE_FILE:-}\" = \"\" ]; then\n\texport COMPOSE_FILE=\"docker/docker-compose.ci.yml:docker/docker-compose.ci.${STACK}.yml\"\nfi\n\n# Colors\nBLUE='\\E[1;34m'\nRED='\\E[1;31m'\nCYAN='\\E[1;36m'\nGREEN='\\E[1;32m'\nRESET='\\E[0m'\nYELLOW='\\E[1;33m'\n\nexport BLUE CYAN GREEN RESET YELLOW\n\necho -e \"${BLUE}❯ ${CYAN}Starting fullstack cypress testing ...${RESET}\"\necho -e \"${BLUE}❯ $(docker compose config)${RESET}\"\n\n# $1: container_name\nget_container_ip () {\n\tlocal container_name=$1\n\tlocal container\n\tlocal ip\n\tcontainer=$(docker compose ps --all -q \"${container_name}\" | tail -n1)\n\tip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \"$container\")\n\techo \"$ip\"\n}\n\n# Bring up a stack, in steps so we can inject IPs everywhere\ndocker compose up -d pdns pdns-db\nPDNS_IP=$(get_container_ip \"pdns\")\necho -e \"${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}\"\n\n# adjust the dnsrouter config\nLOCAL_DNSROUTER_CONFIG=\"$DIR/../../docker/dev/dnsrouter-config.json\"\nrm -rf \"$LOCAL_DNSROUTER_CONFIG.tmp\"\n# IMPORTANT: changes to dnsrouter-config.json will affect this line:\njq --arg a \"$PDNS_IP\" '.servers[0].upstreams[1].upstream = $a' \"$LOCAL_DNSROUTER_CONFIG\" > \"$LOCAL_DNSROUTER_CONFIG.tmp\"\n\ndocker compose up -d dnsrouter\nDNSROUTER_IP=$(get_container_ip \"dnsrouter\")\necho -e \"${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}\"\n\nif [ \"${DNSROUTER_IP:-}\" = \"\" ]; then\n\techo -e \"${RED}❯ ERROR: DNS Router IP is not set${RESET}\"\n\texit 1\nfi\n\n# mount the resolver\nLOCAL_RESOLVE=\"$DIR/../../docker/dev/resolv.conf\"\nrm -rf \"${LOCAL_RESOLVE}\"\nprintf \"nameserver %s\\noptions ndots:0\" \"${DNSROUTER_IP}\" > \"${LOCAL_RESOLVE}\"\n\n# bring up all remaining containers, except cypress!\ndocker compose up -d --remove-orphans stepca squid\ndocker compose pull db-mysql || true # ok to fail\ndocker compose pull db-postgres || true # ok to fail\ndocker compose pull authentik authentik-redis authentik-ldap || true # ok to fail\ndocker compose up -d --remove-orphans --pull=never fullstack\n\n# wait for main container to be healthy\nbash \"$DIR/../wait-healthy\" \"$(docker compose ps --all -q fullstack)\" 120\n\n# Run tests\nrm -rf \"$DIR/../../test/results\"\ndocker compose up --build cypress\n\n# Get results\ndocker cp -L \"$(docker compose ps --all -q cypress):/test/results\" \"$DIR/../../test/\"\ndocker cp -L \"$(docker compose ps --all -q fullstack):/data/logs\" \"$DIR/../../test/results/\"\n\nif [ \"$2\" = \"cleanup\" ]; then\n\techo -e \"${BLUE}❯ ${CYAN}Cleaning up containers ...${RESET}\"\n\tdocker compose down --remove-orphans --volumes -t 30\nfi\n\necho -e \"${BLUE}❯ ${GREEN}Fullstack cypress testing complete${RESET}\"\n\n"
  },
  {
    "path": "scripts/ci/test-and-build",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/../.common.sh\"\n\nTESTING_IMAGE=nginxproxymanager/nginx-full:certbot-node\ndocker pull \"${TESTING_IMAGE}\"\n\n# Test\necho -e \"${BLUE}❯ ${CYAN}Testing backend ...${RESET}\"\ndocker run --rm \\\n\t-v \"$(pwd)/backend:/app\" \\\n\t-w /app \\\n\t\"${TESTING_IMAGE}\" \\\n\tsh -c 'yarn install && yarn lint . && rm -rf node_modules'\necho -e \"${BLUE}❯ ${GREEN}Testing Complete${RESET}\"\n\n# Build\necho -e \"${BLUE}❯ ${CYAN}Building ...${RESET}\"\ndocker build --pull --no-cache --compress \\\n\t-t \"${IMAGE:-nginx-proxy-manager}:${BRANCH_LOWER:-unknown}-ci-${BUILD_NUMBER:-0000}\" \\\n\t-f docker/Dockerfile \\\n\t--progress=plain \\\n\t--build-arg TARGETPLATFORM=linux/amd64 \\\n\t--build-arg BUILDPLATFORM=linux/amd64 \\\n\t--build-arg BUILD_VERSION=\"${BUILD_VERSION:-unknown}\" \\\n\t--build-arg BUILD_COMMIT=\"${BUILD_COMMIT:-unknown}\" \\\n\t--build-arg BUILD_DATE=\"$(date '+%Y-%m-%d %T %Z')\" \\\n\t.\necho -e \"${BLUE}❯ ${GREEN}Building Complete${RESET}\"\n"
  },
  {
    "path": "scripts/cypress-dev",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\n# Ensure docker exists\nif hash docker 2>/dev/null; then\n\tcd \"${DIR}/..\"\n\trm -rf \"$DIR/../test/results\"\n\tdocker compose up --build cypress\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/destroy-dev",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\n# Make sure docker exists\nif hash docker 2>/dev/null; then\n\tcd \"${DIR}/..\"\n\techo -e \"${BLUE}❯ ${CYAN}Destroying Dev Stack ...${RESET}\"\n\tdocker compose down --remove-orphans --volumes\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/docs-build",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\n# Ensure docker exists\nif hash docker 2>/dev/null; then\n\tcd \"${DIR}/..\"\n\techo -e \"${BLUE}❯ ${CYAN}Building Docs ...${RESET}\"\n\tdocker run --rm -e CI=true -v \"$(pwd)/docs:/app/docs\" -w /app/docs node:alpine sh -c \"yarn set version berry && yarn install && yarn build && chown -R $(id -u):$(id -g) /app/docs\"\n\techo -e \"${BLUE}❯ ${GREEN}Building Docs Complete${RESET}\"\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/docs-upload",
    "content": "#!/bin/bash\n\n# Note: This script is designed to be run inside CI builds\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\necho -e \"${BLUE}❯ ${CYAN}Uploading docs in: ${YELLOW}$1${RESET}\"\n\ncd \"$1\" || exit 1\nALL_FILES=$(find . -follow)\n\nfor FILE in $ALL_FILES\ndo\n\t# remove preceding ./\n\tFILE=$(echo \"$FILE\" | sed -E \"s/\\.\\///g\")\n\techo '======================================='\n\techo \"FILE: $FILE\"\n\n\tif [[ -d $FILE ]]; then\n\t\techo \"Skipping $FILE because it's a directory\"\n\telif [[ -f $FILE ]]; then\n\t\tPARAM_STRING=\"--guess-mime-type\"\n\t\tEXT=\"${FILE##*.}\"\n\t\tif [ \"$EXT\" == \"css\" ]; then\n\t\t\tPARAM_STRING=\"-mtext/css\"\n\t\telif [ \"$EXT\" == \"js\" ]; then\n\t\t\tPARAM_STRING=\"-mapplication/javascript\"\n\t\telif [[ \"$EXT\" == \"html\" ]]; then\n\t\t\tPARAM_STRING=\"-mtext/html\"\n\t\telif [[ \"$EXT\" == \"png\" ]]; then\n\t\t\tPARAM_STRING=\"-mimage/png\"\n\t\telif [[ \"$EXT\" == \"jpg\" ]]; then\n\t\t\tPARAM_STRING=\"-mimage/jpg\"\n\t\telif [[ \"$EXT\" == \"svg\" ]]; then\n\t\t\tPARAM_STRING=\"-mimage/svg+xml\"\n\t\tfi\n\n\t\tDEST_FOLDER=$(dirname \"$FILE\")\n\t\tif [ \"$DEST_FOLDER\" == \".\" ]; then\n\t\t\tDEST_FOLDER=\n\t\telse\n\t\t\tDEST_FOLDER=\"${DEST_FOLDER}/\"\n\t\tfi\n\n\t\techo s3cmd -v -f -P \"$PARAM_STRING\" --add-header=\"Cache-Control:public,max-age=604800\" sync \"$FILE\" \"s3://$S3_BUCKET/$DEST_FOLDER\"\n\t\ts3cmd -v -f -P \"$PARAM_STRING\" --add-header=\"Cache-Control:public,max-age=604800\" sync \"$FILE\" \"s3://$S3_BUCKET/$DEST_FOLDER\"\n\t\trc=$?; if [ $rc != 0 ]; then exit $rc; fi\n\tfi\ndone\n\necho -e \"${BLUE}❯ ${GREEN}Upload Complete${RESET}\"\n"
  },
  {
    "path": "scripts/start-dev",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\n# Ensure docker exists\nif hash docker 2>/dev/null; then\n\tcd \"${DIR}/..\"\n\techo -e \"${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}\"\n\techo -e \"${BLUE}❯ $(docker compose config)${RESET}\"\n\n\t# Bring up a stack, in steps so we can inject IPs everywhere\n\tdocker compose up -d pdns pdns-db\n\tPDNS_IP=$(get_container_ip \"pdns\")\n\techo -e \"${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}\"\n\n\t# adjust the dnsrouter config\n\tLOCAL_DNSROUTER_CONFIG=\"$DIR/../docker/dev/dnsrouter-config.json\"\n\trm -rf \"$LOCAL_DNSROUTER_CONFIG.tmp\"\n\t# IMPORTANT: changes to dnsrouter-config.json will affect this line:\n\tjq --arg a \"$PDNS_IP\" '.servers[0].upstreams[1].upstream = $a' \"$LOCAL_DNSROUTER_CONFIG\" > \"$LOCAL_DNSROUTER_CONFIG.tmp\"\n\n\tdocker compose up -d dnsrouter\n\tDNSROUTER_IP=$(get_container_ip \"dnsrouter\")\n\techo -e \"${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}\"\n\n\tif [ \"${DNSROUTER_IP:-}\" = \"\" ]; then\n\t\techo -e \"${RED}❯ ERROR: DNS Router IP is not set${RESET}\"\n\t\texit 1\n\tfi\n\n\t# mount the resolver\n\tLOCAL_RESOLVE=\"$DIR/../docker/dev/resolv.conf\"\n\trm -rf \"${LOCAL_RESOLVE}\"\n\tprintf \"nameserver %s\\noptions ndots:0\" \"${DNSROUTER_IP}\" > \"${LOCAL_RESOLVE}\"\n\n\t# bring up all remaining containers, except cypress!\n\tdocker compose up -d --remove-orphans stepca squid\n\tdocker compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap\n\tdocker compose build --pull --parallel fullstack\n\tdocker compose up -d --remove-orphans fullstack\n\tdocker compose up -d --remove-orphans swagger\n\n\t# wait for main container to be healthy\n\tbash \"$DIR/wait-healthy\" \"$(docker compose ps --all -q fullstack)\" 120\n\n\techo \"\"\n\techo -e \"${CYAN}Admin UI:     http://127.0.0.1:3081${RESET}\"\n\techo -e \"${CYAN}Nginx:        http://127.0.0.1:3080${RESET}\"\n\techo -e \"${CYAN}Swagger Doc:  http://127.0.0.1:3001${RESET}\"\n\techo \"\"\n\n\tif [ \"$1\" == \"-f\" ]; then\n\t\techo -e \"${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}\"\n\t\tdocker logs -f npm2dev.core\n\telse\n\t\techo -e \"${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:\"\n\t\techo \"  docker logs -f npm2dev.core\"\n\tfi\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/stop-dev",
    "content": "#!/bin/bash -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\n# Make sure docker exists\nif hash docker 2>/dev/null; then\n\tcd \"${DIR}/..\"\n\techo -e \"${BLUE}❯ ${CYAN}Stopping Dev Stack ...${RESET}\"\n\tdocker compose down --remove-orphans\nelse\n\techo -e \"${RED}❯ docker command is not available${RESET}\"\nfi\n"
  },
  {
    "path": "scripts/wait-healthy",
    "content": "#!/bin/bash\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n. \"$DIR/.common.sh\"\n\nif [ \"$1\" == \"\" ]; then\n\techo \"Waits for a docker container to be healthy.\"\n\techo \"Usage: $0 docker-container\"\n\texit 1\nfi\n\nSERVICE=$1\nLOOPCOUNT=0\nHEALTHY=\nLIMIT=${2:-90}\n\necho -e \"${BLUE}❯ ${CYAN}Waiting for healthy: ${YELLOW}${SERVICE}${RESET}\"\n\nuntil [ \"${HEALTHY}\" = \"healthy\" ]; do\n\techo -n \".\"\n\tsleep 1\n\tHEALTHY=\"$(docker inspect -f '{{.State.Health.Status}}' $SERVICE)\"\n\t((LOOPCOUNT++))\n\n\tif [ \"$LOOPCOUNT\" == \"$LIMIT\" ]; then\n\t\techo -e \"${BLUE}❯ ${RED}Timed out waiting for healthy${RESET}\"\n\t\tdocker logs --tail 50 \"$SERVICE\"\n\t\texit 1\n\tfi\ndone\n\necho \"\"\necho -e \"${BLUE}❯ ${GREEN}Healthy!${RESET}\"\n"
  },
  {
    "path": "test/.eslintrc.json",
    "content": "{\n\t\"env\": {\n\t\t\"browser\": true,\n\t\t\"es6\": true,\n\t\t\"cypress/globals\": true\n\t},\n\t\"extends\": [\n\t\t\"eslint:recommended\",\n\t\t\"plugin:cypress/recommended\"\n\t],\n\t\"globals\": {\n\t\t\"Atomics\": \"readonly\",\n\t\t\"SharedArrayBuffer\": \"readonly\"\n\t},\n\t\"parserOptions\": {\n\t\t\"ecmaVersion\": 2018,\n\t\t\"sourceType\": \"module\"\n\t},\n\t\"plugins\": [\n\t\t\"cypress\",\n\t\t\"chai-friendly\",\n\t\t\"align-assignments\"\n\t],\n\t\"rules\": {\n\t\t\"indent\": [\n\t\t\t\"error\",\n\t\t\t\"tab\"\n\t\t],\n\t\t\"linebreak-style\": [\n\t\t\t\"error\",\n\t\t\t\"unix\"\n\t\t],\n\t\t\"quotes\": [\n\t\t\t\"error\",\n\t\t\t\"single\"\n\t\t],\n\t\t\"semi\": [\n\t\t\t\"error\",\n\t\t\t\"always\"\n\t\t],\n\t\t\"key-spacing\": [\n\t\t\t\"error\",\n\t\t\t{\n\t\t\t\t\"align\": \"value\"\n\t\t\t}\n\t\t],\n\t\t\"comma-spacing\": [\n\t\t\t\"error\",\n\t\t\t{\n\t\t\t\t\"before\": false,\n\t\t\t\t\"after\": true\n\t\t\t}\n\t\t],\n\t\t\"func-call-spacing\": [\n\t\t\t\"error\",\n\t\t\t\"never\"\n\t\t],\n\t\t\"keyword-spacing\": [\n\t\t\t\"error\",\n\t\t\t{\n\t\t\t\t\"before\": true\n\t\t\t}\n\t\t],\n\t\t\"no-irregular-whitespace\": \"error\",\n\t\t\"cypress/no-assigning-return-values\": \"error\",\n\t\t\"cypress/no-unnecessary-waiting\": \"warn\",\n\t\t\"no-unused-expressions\": 0,\n\t\t\"chai-friendly/no-unused-expressions\": 2,\n\t\t\"align-assignments/align-assignments\": [\n\t\t\t2,\n\t\t\t{\n\t\t\t\t\"requiresOnly\": false\n\t\t\t}\n\t\t]\n\t}\n}\n"
  },
  {
    "path": "test/.gitignore",
    "content": "results/*\ncypress/results/*\n"
  },
  {
    "path": "test/.prettierrc",
    "content": "{\n\t\"printWidth\": 160,\n\t\"tabWidth\": 4,\n\t\"useTabs\": true,\n\t\"semi\": true,\n\t\"singleQuote\": true,\n\t\"bracketSpacing\": true,\n\t\"jsxBracketSameLine\": true,\n\t\"trailingComma\": \"all\",\n\t\"proseWrap\": \"always\"\n}\n"
  },
  {
    "path": "test/README.md",
    "content": "# Cypress Test Suite\n\n## Running Locally\n\n```\ncd nginxproxymanager/test\nyarn install\nyarn run cypress\n```\n\n## VS Code\n\nEditor settings are not committed to the repository, typically because each developer has their own settings. Below is a list of common setting that may help,\nso feel free to try them or ignore them, you are a strong independent developer. You can add settings to either \"user\" or \"workspace\" but we recommend using\n\"workspace\" as each project is different.\n\n### ESLint\n\nThe ESLint extension only works on JavaScript files by default, so add the following to your workspace settings and reload VSCode.\n\n```\n\"eslint.autoFixOnSave\": true,\n\"eslint.validate\": [\n\t{ \"language\": \"javascript\", \"autoFix\": true },\n\t\"html\"\n]\n```\n\n> NOTE: If you've also set the editor.formatOnSave option to true in your settings.json, you'll need to add the following config to prevent running 2 formatting\n> commands on save for JavaScript and TypeScript files:\n\n```\n\"editor.formatOnSave\": true,\n\"[javascript]\": {\n\t\"editor.formatOnSave\": false,\n},\n\"[javascriptreact]\": {\n\t\"editor.formatOnSave\": false,\n},\n\"[typescript]\": {\n\t\"editor.formatOnSave\": false,\n},\n\"[typescriptreact]\": {\n\t\"editor.formatOnSave\": false,\n},\n```\n"
  },
  {
    "path": "test/cypress/Dockerfile",
    "content": "FROM cypress/included:15.11.0\n\n# Disable Cypress CLI colors\nENV FORCE_COLOR=0\nENV NO_COLOR=1\n\n# testssl.sh and mkcert\nRUN wget \"https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz\" -O /tmp/testssl.tgz -q \\\n\t&& tar -xzf /tmp/testssl.tgz -C /tmp \\\n\t&& mv /tmp/testssl.sh-3.2rc4 /testssl \\\n\t&& rm /tmp/testssl.tgz \\\n\t&& apt-get update \\\n\t&& apt-get install -y bsdmainutils curl dnsutils \\\n\t&& apt-get clean \\\n\t&& rm -rf /var/lib/apt/lists/* \\\n\t&& wget \"https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64\" -O /bin/mkcert \\\n\t&& chmod +x /bin/mkcert\n\nCOPY --chown=1000 ./test /test\nWORKDIR /test\nRUN yarn install && yarn cache clean\nENTRYPOINT []\nCMD [\"cypress\", \"run\"]\n"
  },
  {
    "path": "test/cypress/config/ci.mjs",
    "content": "import { defineConfig } from 'cypress';\nimport pluginSetup from '../plugins/index.mjs';\n\nexport default defineConfig({\n\trequestTimeout: 30000,\n\tdefaultCommandTimeout: 20000,\n\treporter: \"cypress-multi-reporters\",\n\treporterOptions: {\n\t\tconfigFile: \"multi-reporter.json\"\n\t},\n\tvideo: true,\n\tvideosFolder: \"results/videos\",\n\tscreenshotsFolder: \"results/screenshots\",\n\te2e: {\n\t\tsetupNodeEvents(on, config) {\n\t\t\treturn pluginSetup(on, config);\n\t\t},\n\t\tenv: {\n\t\t\tswaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`,\n\t\t},\n\t\tbaseUrl: \"http://fullstack:81\",\n\t}\n});\n"
  },
  {
    "path": "test/cypress/config/dev.mjs",
    "content": "import { defineConfig } from 'cypress';\nimport pluginSetup from '../plugins/index.mjs';\n\nexport default defineConfig({\n\trequestTimeout: 30000,\n\tdefaultCommandTimeout: 20000,\n\treporter: \"cypress-multi-reporters\",\n\treporterOptions: {\n\t\tconfigFile: \"multi-reporter.json\"\n\t},\n\tvideo: true,\n\tvideosFolder: \"results/videos\",\n\tscreenshotsFolder: \"results/screenshots\",\n\te2e: {\n\t\tsetupNodeEvents(on, config) {\n\t\t\treturn pluginSetup(on, config);\n\t\t},\n\t\tenv: {\n\t\t\tswaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`,\n\t\t},\n\t\tbaseUrl: \"http://127.0.0.1:3081\",\n\t}\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Certificates.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Certificates endpoints', () => {\n\tlet token;\n\tlet certID;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Validate custom certificate', () => {\n\t\tcy.task('backendApiPostFiles', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates/validate',\n\t\t\tfiles:  {\n\t\t\t\tcertificate: 'test.example.com.pem',\n\t\t\t\tcertificate_key: 'test.example.com-key.pem',\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data);\n\t\t\texpect(data).to.have.property('certificate');\n\t\t\texpect(data).to.have.property('certificate_key');\n\t\t});\n\t});\n\n\tit('Custom certificate lifecycle', () => {\n\t\t// Create custom cert\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates',\n\t\t\tdata:  {\n\t\t\t\tprovider: \"other\",\n\t\t\t\tnice_name: \"Test Certificate\",\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\tcertID = data.id;\n\n\t\t\t// Upload files\n\t\t\tcy.task('backendApiPostFiles', {\n\t\t\t\ttoken: token,\n\t\t\t\tpath:  `/api/nginx/certificates/${certID}/upload`,\n\t\t\t\tfiles:  {\n\t\t\t\t\tcertificate: 'test.example.com.pem',\n\t\t\t\t\tcertificate_key: 'test.example.com-key.pem',\n\t\t\t\t},\n\t\t\t}).then((data) => {\n\t\t\t\tcy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);\n\t\t\t\texpect(data).to.have.property('certificate');\n\t\t\t\texpect(data).to.have.property('certificate_key');\n\n\t\t\t\t// Get all certs\n\t\t\t\tcy.task('backendApiGet', {\n\t\t\t\t\ttoken: token,\n\t\t\t\t\tpath:  '/api/nginx/certificates?expand=owner'\n\t\t\t\t}).then((data) => {\n\t\t\t\t\tcy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);\n\t\t\t\t\texpect(data.length).to.be.greaterThan(0);\n\n\t\t\t\t\t// Delete cert\n\t\t\t\t\tcy.task('backendApiDelete', {\n\t\t\t\t\t\ttoken: token,\n\t\t\t\t\t\tpath:  `/api/nginx/certificates/${certID}`\n\t\t\t\t\t}).then((data) => {\n\t\t\t\t\t\tcy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);\n\t\t\t\t\t\texpect(data).to.be.equal(true);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t});\n\n\tit('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates',\n\t\t\tdata:  {\n\t\t\t\tdomain_names: ['test.com\"||echo hello-world||\\\\\\\\n test.com\"'],\n\t\t\t\tmeta:         {\n\t\t\t\t\tdns_challenge: false,\n\t\t\t\t},\n\t\t\t\tprovider: 'letsencrypt',\n\t\t\t},\n\t\t\treturnOnError: true,\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);\n\t\t\texpect(data).to.have.property('error');\n\t\t\texpect(data.error).to.have.property('message');\n\t\t\texpect(data.error).to.have.property('code');\n\t\t\texpect(data.error.code).to.equal(400);\n\t\t\texpect(data.error.message).to.contain('data/domain_names/0 must match pattern');\n\t\t});\n\t});\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Dashboard.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Dashboard endpoints', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Should be able to get host counts', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/reports/hosts'\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/reports/hosts', data);\n\t\t\texpect(data).to.have.property('dead');\n\t\t\texpect(data).to.have.property('proxy');\n\t\t\texpect(data).to.have.property('redirection');\n\t\t\texpect(data).to.have.property('stream');\n\t\t});\n\t});\n\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/FullCertProvision.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Full Certificate Provisions', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Should be able to create new http certificate', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates',\n\t\t\tdata:  {\n\t\t\t\tdomain_names: [\n\t\t\t\t\t'website1.example.com'\n\t\t\t\t],\n\t\t\t\tmeta: {\n\t\t\t\t\tdns_challenge: false\n\t\t\t\t},\n\t\t\t\tprovider: 'letsencrypt'\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data.provider).to.be.equal('letsencrypt');\n\t\t});\n\t});\n\n\tit('Should be able to create new DNS certificate with Powerdns', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates',\n\t\t\tdata:  {\n\t\t\t\tdomain_names: [\n\t\t\t\t\t'website2.example.com'\n\t\t\t\t],\n\t\t\t\tmeta: {\n\t\t\t\t\tdns_challenge: true,\n\t\t\t\t\tdns_provider: 'powerdns',\n\t\t\t\t\tdns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\\r\\ndns_powerdns_api_key = npm',\n\t\t\t\t\tpropagation_seconds: 5,\n\t\t\t\t},\n\t\t\t\tprovider: 'letsencrypt'\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data.provider).to.be.equal('letsencrypt');\n\t\t\texpect(data.meta.dns_provider).to.be.equal('powerdns');\n\t\t});\n\t});\n\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Health.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Basic API checks', () => {\n\tit('Should return a valid health payload', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\tpath: '/api/',\n\t\t}).then((data) => {\n\t\t\t// Check the swagger schema:\n\t\t\tcy.validateSwaggerSchema('get', 200, '/', data);\n\t\t});\n\t});\n\n\tit('Should return a valid schema payload', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\tpath: `/api/schema?ts=${Date.now()}`,\n\t\t}).then((data) => {\n\t\t\texpect(data.openapi).to.be.equal('3.1.0');\n\t\t});\n\t});\n\n\tit('Should return a valid payload for version check', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\tpath: `/api/version/check?ts=${Date.now()}`,\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/version/check', data);\n\t\t});\n\t});\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Ldap.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('LDAP with Authentik', () => {\n\tlet _token;\n\tif (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {\n\n\t\tbefore(() => {\n\t\t\tcy.resetUsers();\n\t\t\tcy.getToken().then((tok) => {\n\t\t\t\t_token = tok;\n\n\t\t\t\t// cy.task('backendApiPut', {\n\t\t\t\t// \ttoken: token,\n\t\t\t\t// \tpath:  '/api/settings/ldap-auth',\n\t\t\t\t// \tdata:  {\n\t\t\t\t// \t\tvalue: {\n\t\t\t\t// \t\t\thost: 'authentik-ldap:3389',\n\t\t\t\t// \t\t\tbase_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io',\n\t\t\t\t// \t\t\tuser_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io',\n\t\t\t\t// \t\t\temail_property: 'mail',\n\t\t\t\t// \t\t\tname_property: 'sn',\n\t\t\t\t// \t\t\tself_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))',\n\t\t\t\t// \t\t\tauto_create_user: true\n\t\t\t\t// \t\t}\n\t\t\t\t// \t}\n\t\t\t\t// }).then((data) => {\n\t\t\t\t// \tcy.validateSwaggerSchema('put', 200, '/settings/{name}', data);\n\t\t\t\t// \texpect(data.result).to.have.property('id');\n\t\t\t\t// \texpect(data.result.id).to.be.greaterThan(0);\n\t\t\t\t// });\n\n\t\t\t\t// cy.task('backendApiPut', {\n\t\t\t\t// \ttoken: token,\n\t\t\t\t// \tpath:  '/api/settings/auth-methods',\n\t\t\t\t// \tdata:  {\n\t\t\t\t// \t\tvalue: [\n\t\t\t\t// \t\t\t'local',\n\t\t\t\t// \t\t\t'ldap'\n\t\t\t\t// \t\t]\n\t\t\t\t// \t}\n\t\t\t\t// }).then((data) => {\n\t\t\t\t// \tcy.validateSwaggerSchema('put', 200, '/settings/{name}', data);\n\t\t\t\t// \texpect(data.result).to.have.property('id');\n\t\t\t\t// \texpect(data.result.id).to.be.greaterThan(0);\n\t\t\t\t// });\n\t\t\t});\n\t\t});\n\n\t\tit.skip('Should log in with LDAP', () => {\n\t\t\t// cy.task('backendApiPost', {\n\t\t\t// \ttoken: token,\n\t\t\t// \tpath:  '/api/auth',\n\t\t\t// \tdata:  {\n\t\t\t// \t\t// Authentik LDAP creds:\n\t\t\t// \t\ttype: 'ldap',\n\t\t\t// \t\tidentity: 'cypress',\n\t\t\t// \t\tsecret: 'fqXBfUYqHvYqiwBHWW7f'\n\t\t\t// \t}\n\t\t\t// }).then((data) => {\n\t\t\t// \tcy.validateSwaggerSchema('post', 200, '/auth', data);\n\t\t\t// \texpect(data.result).to.have.property('token');\n\t\t\t// });\n\t\t});\n\t}\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/OAuth.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('OAuth with Authentik', () => {\n\tlet _token;\n\tif (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {\n\n\t\tbefore(() => {\n\t\t\tcy.getToken().then((tok) => {\n\t\t\t\t_token = tok;\n\n\t\t\t\t// cy.task('backendApiPut', {\n\t\t\t\t// \ttoken: token,\n\t\t\t\t// \tpath:  '/api/settings/oauth-auth',\n\t\t\t\t// \tdata:  {\n\t\t\t\t// \t\tvalue: {\n\t\t\t\t// \t\t\tclient_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU',\n\t\t\t\t// \t\t\tclient_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq',\n\t\t\t\t// \t\t\tauthorization_url: 'http://authentik:9000/application/o/authorize/',\n\t\t\t\t// \t\t\tresource_url: 'http://authentik:9000/application/o/userinfo/',\n\t\t\t\t// \t\t\ttoken_url: 'http://authentik:9000/application/o/token/',\n\t\t\t\t// \t\t\tlogout_url: 'http://authentik:9000/application/o/npm/end-session/',\n\t\t\t\t// \t\t\tidentifier: 'preferred_username',\n\t\t\t\t// \t\t\tscopes: [],\n\t\t\t\t// \t\t\tauto_create_user: true\n\t\t\t\t// \t\t}\n\t\t\t\t// \t}\n\t\t\t\t// }).then((data) => {\n\t\t\t\t// \tcy.validateSwaggerSchema('put', 200, '/settings/{name}', data);\n\t\t\t\t// \texpect(data.result).to.have.property('id');\n\t\t\t\t// \texpect(data.result.id).to.be.greaterThan(0);\n\t\t\t\t// });\n\n\t\t\t\t// cy.task('backendApiPut', {\n\t\t\t\t// \ttoken: token,\n\t\t\t\t// \tpath:  '/api/settings/auth-methods',\n\t\t\t\t// \tdata:  {\n\t\t\t\t// \t\tvalue: [\n\t\t\t\t// \t\t\t'local',\n\t\t\t\t// \t\t\t'oauth'\n\t\t\t\t// \t\t]\n\t\t\t\t// \t}\n\t\t\t\t// }).then((data) => {\n\t\t\t\t// \tcy.validateSwaggerSchema('put', 200, '/settings/{name}', data);\n\t\t\t\t// \texpect(data.result).to.have.property('id');\n\t\t\t\t// \texpect(data.result.id).to.be.greaterThan(0);\n\t\t\t\t// });\n\t\t\t});\n\t\t});\n\n\t\tit.skip('Should log in with OAuth', () => {\n\t\t\t// cy.task('backendApiGet', {\n\t\t\t// \tpath:  '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')),\n\t\t\t// }).then((data) => {\n\t\t\t// \texpect(data).to.have.property('result');\n\n\t\t\t// \tcy.origin('http://authentik:9000', {args: data.result}, (url) => {\n\t\t\t// \t\tcy.visit(url);\n\t\t\t// \t\tcy.get('ak-flow-executor')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('ak-stage-identification')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('input[name=\"uidField\"]', { visible: true })\n\t\t\t// \t\t.type('cypress');\n\n\t\t\t// \tcy.get('ak-flow-executor')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('ak-stage-identification')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('button[type=\"submit\"]', { visible: true })\n\t\t\t// \t\t.click();\n\n\t\t\t// \tcy.get('ak-flow-executor')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('ak-stage-password')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('input[name=\"password\"]', { visible: true })\n\t\t\t// \t\t.type('fqXBfUYqHvYqiwBHWW7f');\n\n\t\t\t// \tcy.get('ak-flow-executor')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('ak-stage-password')\n\t\t\t// \t\t.shadow()\n\t\t\t// \t\t.find('button[type=\"submit\"]', { visible: true })\n\t\t\t// \t\t.click();\n\t\t\t// \t})\n\n\t\t\t// \t// we should be logged in\n\t\t\t// \tcy.get('#root p.chakra-text')\n\t\t\t// \t\t.first()\n\t\t\t// \t\t.should('have.text', 'Nginx Proxy Manager');\n\n\t\t\t// \t// logout:\n\t\t\t// \tcy.clearLocalStorage();\n\t\t\t// });\n\t\t});\n\t}\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/ProxyHosts.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Proxy Hosts endpoints', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Should be able to create a http host', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/proxy-hosts',\n\t\t\tdata:  {\n\t\t\t\tdomain_names:   ['test.example.com'],\n\t\t\t\tforward_scheme: 'http',\n\t\t\t\tforward_host:   '1.1.1.1',\n\t\t\t\tforward_port:   80,\n\t\t\t\taccess_list_id: '0',\n\t\t\t\tcertificate_id: 0,\n\t\t\t\tmeta:           {\n\t\t\t\t\tdns_challenge: false\n\t\t\t\t},\n\t\t\t\tadvanced_config:         '',\n\t\t\t\tlocations:               [],\n\t\t\t\tblock_exploits:          false,\n\t\t\t\tcaching_enabled:         false,\n\t\t\t\tallow_websocket_upgrade: false,\n\t\t\t\thttp2_support:           false,\n\t\t\t\thsts_enabled:            false,\n\t\t\t\thsts_subdomains:         false,\n\t\t\t\tssl_forced:              false\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data).to.have.property('enabled');\n\t\t\texpect(data).to.have.property(\"enabled\", true);\n\t\t\texpect(data).to.have.property('meta');\n\t\t\texpect(typeof data.meta.nginx_online).to.be.equal('undefined');\n\t\t});\n\t});\n\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Settings.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Settings endpoints', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Get all settings', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings',\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/settings', data);\n\t\t\texpect(data.length).to.be.greaterThan(0);\n\t\t});\n\t});\n\n\tit('Get default-site setting', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t});\n\t});\n\n\tit('Default Site congratulations', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t\tdata: {\n\t\t\t\tvalue: 'congratulations',\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t\texpect(data).to.have.property('value');\n\t\t\texpect(data.value).to.be.equal('congratulations');\n\t\t});\n\t});\n\n\tit('Default Site 404', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t\tdata: {\n\t\t\t\tvalue: '404',\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t\texpect(data).to.have.property('value');\n\t\t\texpect(data.value).to.be.equal('404');\n\t\t});\n\t});\n\n\tit('Default Site 444', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t\tdata: {\n\t\t\t\tvalue: '444',\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t\texpect(data).to.have.property('value');\n\t\t\texpect(data.value).to.be.equal('444');\n\t\t});\n\t});\n\n\tit('Default Site redirect', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t\tdata: {\n\t\t\t\tvalue: 'redirect',\n\t\t\t\tmeta: {\n\t\t\t\t\tredirect: 'https://www.google.com',\n\t\t\t\t},\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t\texpect(data).to.have.property('value');\n\t\t\texpect(data.value).to.be.equal('redirect');\n\t\t\texpect(data).to.have.property('meta');\n\t\t\texpect(data.meta).to.have.property('redirect');\n\t\t\texpect(data.meta.redirect).to.be.equal('https://www.google.com');\n\t\t});\n\t});\n\n\tit('Default Site html', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/settings/default-site',\n\t\t\tdata: {\n\t\t\t\tvalue: 'html',\n\t\t\t\tmeta: {\n\t\t\t\t\thtml: '<p>hello world</p>'\n\t\t\t\t},\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.equal('default-site');\n\t\t\texpect(data).to.have.property('value');\n\t\t\texpect(data.value).to.be.equal('html');\n\t\t\texpect(data).to.have.property('meta');\n\t\t\texpect(data.meta).to.have.property('html');\n\t\t\texpect(data.meta.html).to.be.equal('<p>hello world</p>');\n\t\t});\n\t});\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Streams.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Streams', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t\t// Set default site content\n\t\t\tcy.task('backendApiPut', {\n\t\t\t\ttoken: token,\n\t\t\t\tpath:  '/api/settings/default-site',\n\t\t\t\tdata: {\n\t\t\t\t\tvalue: 'html',\n\t\t\t\t\tmeta: {\n\t\t\t\t\t\thtml: '<p>yay it works</p>'\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}).then((data) => {\n\t\t\t\tcy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);\n\t\t\t});\n\t\t});\n\n\t\t// Create a custom cert pair\n\t\tcy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => {\n\t\t\texpect(result.exitCode).to.eq(0);\n\t\t\t// Install CA\n\t\t\tcy.exec('mkcert -install').then((result) => {\n\t\t\t\texpect(result.exitCode).to.eq(0);\n\t\t\t});\n\t\t});\n\n\t\tcy.exec('rm -f /test/results/testssl.json');\n\t});\n\n\tit('Should be able to create TCP Stream', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/streams',\n\t\t\tdata:  {\n\t\t\t\tincoming_port: 1500,\n\t\t\t\tforwarding_host: '127.0.0.1',\n\t\t\t\tforwarding_port: 80,\n\t\t\t\tcertificate_id: 0,\n\t\t\t\tmeta: {},\n\t\t\t\ttcp_forwarding: true,\n\t\t\t\tudp_forwarding: false\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/streams', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data).to.have.property('enabled', true);\n\t\t\texpect(data).to.have.property('tcp_forwarding', true);\n\t\t\texpect(data).to.have.property('udp_forwarding', false);\n\n\t\t\tcy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => {\n\t\t\t\texpect(result.exitCode).to.eq(0);\n\t\t\t\texpect(result.stdout).to.contain('yay it works');\n\t\t\t});\n\t\t});\n\t});\n\n\tit('Should be able to create UDP Stream', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/streams',\n\t\t\tdata:  {\n\t\t\t\tincoming_port: 1501,\n\t\t\t\tforwarding_host: '127.0.0.1',\n\t\t\t\tforwarding_port: 80,\n\t\t\t\tcertificate_id: 0,\n\t\t\t\tmeta: {},\n\t\t\t\ttcp_forwarding: false,\n\t\t\t\tudp_forwarding: true\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/streams', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data).to.have.property('enabled', true);\n\t\t\texpect(data).to.have.property('tcp_forwarding', false);\n\t\t\texpect(data).to.have.property('udp_forwarding', true);\n\t\t});\n\t});\n\n\tit('Should be able to create TCP/UDP Stream', () => {\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/streams',\n\t\t\tdata:  {\n\t\t\t\tincoming_port: 1502,\n\t\t\t\tforwarding_host: '127.0.0.1',\n\t\t\t\tforwarding_port: 80,\n\t\t\t\tcertificate_id: 0,\n\t\t\t\tmeta: {},\n\t\t\t\ttcp_forwarding: true,\n\t\t\t\tudp_forwarding: true\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/streams', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data).to.have.property('enabled', true);\n\t\t\texpect(data).to.have.property('tcp_forwarding', true);\n\t\t\texpect(data).to.have.property('udp_forwarding', true);\n\n\t\t\tcy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => {\n\t\t\t\texpect(result.exitCode).to.eq(0);\n\t\t\t\texpect(result.stdout).to.contain('yay it works');\n\t\t\t});\n\t\t});\n\t});\n\n\tit('Should be able to create SSL TCP Stream', () => {\n\t\tlet certID = 0;\n\n\t\t// Create custom cert\n\t\tcy.task('backendApiPost', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/certificates',\n\t\t\tdata:  {\n\t\t\t\tprovider: \"other\",\n\t\t\t\tnice_name: \"Custom Certificate for SSL Stream\",\n\t\t\t},\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\tcertID = data.id;\n\n\t\t\t// Upload files\n\t\t\tcy.task('backendApiPostFiles', {\n\t\t\t\ttoken: token,\n\t\t\t\tpath:  `/api/nginx/certificates/${certID}/upload`,\n\t\t\t\tfiles:  {\n\t\t\t\t\tcertificate: 'website1.pem',\n\t\t\t\t\tcertificate_key: 'website1.key.pem',\n\t\t\t\t},\n\t\t\t}).then((data) => {\n\t\t\t\tcy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);\n\t\t\t\texpect(data).to.have.property('certificate');\n\t\t\t\texpect(data).to.have.property('certificate_key');\n\n\t\t\t\t// Create the stream\n\t\t\t\tcy.task('backendApiPost', {\n\t\t\t\t\ttoken: token,\n\t\t\t\t\tpath:  '/api/nginx/streams',\n\t\t\t\t\tdata:  {\n\t\t\t\t\t\tincoming_port: 1503,\n\t\t\t\t\t\tforwarding_host: '127.0.0.1',\n\t\t\t\t\t\tforwarding_port: 80,\n\t\t\t\t\t\tcertificate_id: certID,\n\t\t\t\t\t\tmeta: {},\n\t\t\t\t\t\ttcp_forwarding: true,\n\t\t\t\t\t\tudp_forwarding: false\n\t\t\t\t\t}\n\t\t\t\t}).then((data) => {\n\t\t\t\t\tcy.validateSwaggerSchema('post', 201, '/nginx/streams', data);\n\t\t\t\t\texpect(data).to.have.property('id');\n\t\t\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\t\t\texpect(data).to.have.property(\"enabled\", true);\n\t\t\t\t\texpect(data).to.have.property('tcp_forwarding', true);\n\t\t\t\t\texpect(data).to.have.property('udp_forwarding', false);\n\t\t\t\t\texpect(data).to.have.property('certificate_id', certID);\n\n\t\t\t\t\t// Check the ssl termination\n\t\t\t\t\tcy.task('log', '[testssl.sh] Running ...');\n\t\t\t\t\tcy.exec('/testssl/testssl.sh --quiet --add-ca=\"$(/bin/mkcert -CAROOT)/rootCA.pem\" --jsonfile=/test/results/testssl.json website1.example.com:1503', {\n\t\t\t\t\t\ttimeout: 120000, // 2 minutes\n\t\t\t\t\t}).then((result) => {\n\t\t\t\t\t\tcy.task('log', `[testssl.sh] ${result.stdout}`);\n\n\t\t\t\t\t\tconst allowedSeverities = [\"INFO\", \"OK\", \"LOW\", \"MEDIUM\"];\n\t\t\t\t\t\tconst ignoredIDs = [\n\t\t\t\t\t\t\t'cert_chain_of_trust',\n\t\t\t\t\t\t\t'cert_extlifeSpan',\n\t\t\t\t\t\t\t'cert_revocation',\n\t\t\t\t\t\t\t'engine_problem',\n\t\t\t\t\t\t\t'overall_grade',\n\t\t\t\t\t\t];\n\n\t\t\t\t\t\tcy.readFile('/test/results/testssl.json').then((data) => {\n\t\t\t\t\t\t\t// Parse each array item\n\t\t\t\t\t\t\tfor (let i = 0; i < data.length; i++) {\n\t\t\t\t\t\t\t\tconst item = data[i];\n\t\t\t\t\t\t\t\tif (ignoredIDs.includes(item.id)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\texpect(item.severity).to.be.oneOf(allowedSeverities);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t});\n\n\tit('Should be able to List Streams', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/nginx/streams?expand=owner,certificate',\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/nginx/streams', data);\n\t\t\texpect(data.length).to.be.greaterThan(0);\n\t\t\texpect(data[0]).to.have.property('id');\n\t\t\texpect(data[0]).to.have.property('enabled');\n\t\t});\n\t});\n\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/SwaggerSchema.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Swagger Schema Linting', () => {\n\tit('Should be a completely valid schema', () => {\n\t\tcy.validateSwaggerFile('/api/schema', 'results/swagger-schema.json');\n\t});\n});\n"
  },
  {
    "path": "test/cypress/e2e/api/Users.cy.js",
    "content": "/// <reference types=\"cypress\" />\n\ndescribe('Users endpoints', () => {\n\tlet token;\n\n\tbefore(() => {\n\t\tcy.resetUsers();\n\t\tcy.getToken().then((tok) => {\n\t\t\ttoken = tok;\n\t\t});\n\t});\n\n\tit('Should be able to get yourself', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/users/me'\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/users/{userID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t});\n\t});\n\n\tit('Should be able to get all users', () => {\n\t\tcy.task('backendApiGet', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/users'\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('get', 200, '/users', data);\n\t\t\texpect(data.length).to.be.greaterThan(0);\n\t\t});\n\t});\n\n\tit('Should be able to update yourself', () => {\n\t\tcy.task('backendApiPut', {\n\t\t\ttoken: token,\n\t\t\tpath:  '/api/users/me',\n\t\t\tdata:  {\n\t\t\t\tname: 'changed name'\n\t\t\t}\n\t\t}).then((data) => {\n\t\t\tcy.validateSwaggerSchema('put', 200, '/users/{userID}', data);\n\t\t\texpect(data).to.have.property('id');\n\t\t\texpect(data.id).to.be.greaterThan(0);\n\t\t\texpect(data.name).to.be.equal('changed name');\n\t\t});\n\t});\n\n});\n"
  },
  {
    "path": "test/cypress/fixtures/test.example.com-key.pem",
    "content": "-----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"
  },
  {
    "path": "test/cypress/fixtures/test.example.com.pem",
    "content": "-----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"
  },
  {
    "path": "test/cypress/plugins/backendApi/client.mjs",
    "content": "import axios from \"axios\";\nimport logger from \"./logger.mjs\";\n\nconst BackendApi = function (config, token) {\n\tthis.config = config;\n\tthis.token = token;\n\n\tthis.axios = axios.create({\n\t\tbaseURL: config.baseUrl,\n\t\ttimeout: 90000,\n\t});\n};\n\n/**\n * @param {string} token\n */\nBackendApi.prototype.setToken = function (token) {\n\tthis.token = token;\n};\n\n/**\n * @param {bool} returnOnError\n */\nBackendApi.prototype._prepareOptions = function (returnOnError) {\n\tconst options = {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t};\n\tif (this.token) {\n\t\toptions.headers.Authorization = `Bearer ${this.token}`;\n\t}\n\tif (returnOnError) {\n\t\toptions.validateStatus = () => true;\n\t}\n\treturn options;\n};\n\n/**\n * @param {*} response\n * @param {function} resolve\n * @param {function} reject\n * @param {bool} returnOnError\n */\nBackendApi.prototype._handleResponse = (\n\tresponse,\n\tresolve,\n\treject,\n\treturnOnError,\n) => {\n\tlogger(\"Response data:\", response.data);\n\tif (\n\t\t!returnOnError &&\n\t\ttypeof response.data === \"object\" &&\n\t\ttypeof response.data.error === \"object\"\n\t) {\n\t\tif (\n\t\t\ttypeof response.data === \"object\" &&\n\t\t\ttypeof response.data.error === \"object\" &&\n\t\t\ttypeof response.data.error.message !== \"undefined\"\n\t\t) {\n\t\t\treject(\n\t\t\t\tnew Error(\n\t\t\t\t\t`${response.data.error.code}: ${response.data.error.message}`,\n\t\t\t\t),\n\t\t\t);\n\t\t} else {\n\t\t\treject(new Error(`Error ${response.status}`));\n\t\t}\n\t} else {\n\t\tresolve(response.data);\n\t}\n};\n\n/**\n * @param {*} err\n * @param {function} resolve\n * @param {function} reject\n * @param {bool} returnOnError\n */\nBackendApi.prototype._handleError = (err, resolve, reject, returnOnError) => {\n\tlogger(\"Axios Error:\", err);\n\tif (returnOnError) {\n\t\tresolve(typeof err.response.data !== \"undefined\" ? err.response.data : err);\n\t} else {\n\t\treject(err);\n\t}\n};\n\n/**\n * @param {string} method\n * @param {string} path\n * @param {bool}   [returnOnError]\n * @param {*}      [data]\n * @returns {Promise<object>}\n */\nBackendApi.prototype.request = function (method, path, returnOnError, data) {\n\tlogger(method.toUpperCase(), path);\n\tconst options = this._prepareOptions(returnOnError);\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst opts = {\n\t\t\tmethod: method,\n\t\t\turl: path,\n\t\t\t...options,\n\t\t};\n\t\tif (data !== undefined && data !== null) {\n\t\t\topts.data = data;\n\t\t}\n\n\t\tthis.axios(opts)\n\t\t\t.then((response) => {\n\t\t\t\tthis._handleResponse(response, resolve, reject, returnOnError);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tthis._handleError(err, resolve, reject, returnOnError);\n\t\t\t});\n\t});\n};\n\n/**\n * @param {string} path\n * @param {form}   form\n * @param {bool}   [returnOnError]\n * @returns {Promise<object>}\n */\nBackendApi.prototype.postForm = function (path, form, returnOnError) {\n\tlogger(\"POST\", this.config.baseUrl + path);\n\tconst options = this._prepareOptions(returnOnError);\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst opts = {\n\t\t\t...options,\n\t\t\t...form.getHeaders(),\n\t\t};\n\n\t\tthis.axios\n\t\t\t.post(path, form, opts)\n\t\t\t.then((response) => {\n\t\t\t\tthis._handleResponse(response, resolve, reject, returnOnError);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tthis._handleError(err, resolve, reject, returnOnError);\n\t\t\t});\n\t});\n};\n\nexport default BackendApi;\n"
  },
  {
    "path": "test/cypress/plugins/backendApi/logger.mjs",
    "content": "const log = (...args) => {\n\tconst arr = args;\n\tarr.unshift(\"[Backend API]\");\n\tconsole.log(...arr);\n};\n\nexport default log;\n"
  },
  {
    "path": "test/cypress/plugins/backendApi/task.mjs",
    "content": "import fs from \"node:fs\";\nimport FormData from \"form-data\";\nimport Client from \"./client.mjs\";\nimport logger from \"./logger.mjs\";\n\nexport default (config) => {\n\tlogger(\"Client Ready using\", config.baseUrl);\n\n\treturn {\n\t\t/**\n\t\t * @param   {object}    options\n\t\t * @param   {string}    options.path         API path\n\t\t * @param   {string}    [options.token]      JWT\n\t\t * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors\n\t\t * @returns {string}\n\t\t */\n\t\tbackendApiGet: (options) => {\n\t\t\tconst api = new Client(config);\n\t\t\tapi.setToken(options.token);\n\t\t\treturn api.request(\"get\", options.path, options.returnOnError || false);\n\t\t},\n\n\t\t/**\n\t\t * @param   {object}    options\n\t\t * @param   {string}    options.token        JWT\n\t\t * @param   {string}    options.path         API path\n\t\t * @param   {object}    options.data\n\t\t * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors\n\t\t * @returns {string}\n\t\t */\n\t\tbackendApiPost: (options) => {\n\t\t\tconst api = new Client(config);\n\t\t\tapi.setToken(options.token);\n\t\t\treturn api.request(\n\t\t\t\t\"post\",\n\t\t\t\toptions.path,\n\t\t\t\toptions.returnOnError || false,\n\t\t\t\toptions.data,\n\t\t\t);\n\t\t},\n\n\t\t/**\n\t\t * @param   {object}    options\n\t\t * @param   {string}    options.token        JWT\n\t\t * @param   {string}    options.path         API path\n\t\t * @param   {object}    options.files\n\t\t * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors\n\t\t * @returns {string}\n\t\t */\n\t\tbackendApiPostFiles: (options) => {\n\t\t\tconst api = new Client(config);\n\t\t\tapi.setToken(options.token);\n\n\t\t\tconst form = new FormData();\n\t\t\tfor (const [key, value] of Object.entries(options.files)) {\n\t\t\t\tform.append(\n\t\t\t\t\tkey,\n\t\t\t\t\tfs.createReadStream(`${config.fixturesFolder}/${value}`),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn api.postForm(options.path, form, options.returnOnError || false);\n\t\t},\n\n\t\t/**\n\t\t * @param   {object}    options\n\t\t * @param   {string}    options.token        JWT\n\t\t * @param   {string}    options.path         API path\n\t\t * @param   {object}    options.data\n\t\t * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors\n\t\t * @returns {string}\n\t\t */\n\t\tbackendApiPut: (options) => {\n\t\t\tconst api = new Client(config);\n\t\t\tapi.setToken(options.token);\n\t\t\treturn api.request(\n\t\t\t\t\"put\",\n\t\t\t\toptions.path,\n\t\t\t\toptions.returnOnError || false,\n\t\t\t\toptions.data,\n\t\t\t);\n\t\t},\n\n\t\t/**\n\t\t * @param   {object}    options\n\t\t * @param   {string}    options.token        JWT\n\t\t * @param   {string}    options.path         API path\n\t\t * @param   {bool}      [options.returnOnError] If true, will return instead of throwing errors\n\t\t * @returns {string}\n\t\t */\n\t\tbackendApiDelete: (options) => {\n\t\t\tconst api = new Client(config);\n\t\t\tapi.setToken(options.token);\n\t\t\treturn api.request(\n\t\t\t\t\"delete\",\n\t\t\t\toptions.path,\n\t\t\t\toptions.returnOnError || false,\n\t\t\t);\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "test/cypress/plugins/index.mjs",
    "content": "import { SwaggerValidation } from \"@jc21/cypress-swagger-validation\";\nimport chalk from \"chalk\";\nimport backendTask from \"./backendApi/task.mjs\";\n\nexport default (on, config) => {\n\t// Replace swaggerBase config var wildcard\n\tif (typeof config.env.swaggerBase !== \"undefined\") {\n\t\tconfig.env.swaggerBase = config.env.swaggerBase.replace(\n\t\t\t\"{{baseUrl}}\",\n\t\t\tconfig.baseUrl,\n\t\t);\n\t}\n\n\t// Plugin Events\n\ton(\"task\", SwaggerValidation(config));\n\ton(\"task\", backendTask(config));\n\ton(\"task\", {\n\t\tlog(message) {\n\t\t\tconsole.log(\n\t\t\t\t`${chalk.cyan.bold(\"[\")}${chalk.blue.bold(\"LOG\")}${chalk.cyan.bold(\"]\")} ${chalk.red.bold(message)}`,\n\t\t\t);\n\t\t\treturn null;\n\t\t},\n\t});\n\n\treturn config;\n};\n"
  },
  {
    "path": "test/cypress/support/commands.mjs",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n\nimport 'cypress-wait-until';\n\nCypress.Commands.add('randomString', (length) => {\n\tlet result = '';\n\tconst characters = 'ABCDEFGHIJK LMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\tconst charactersLength = characters.length;\n\tfor (let i = 0; i < length; i++) {\n\t\tresult += characters.charAt(Math.floor(Math.random() * charactersLength));\n\t}\n\treturn result;\n});\n\n/**\n * Check the swagger schema file:\n *\n * @param {string}  url\n * @param {string}  savePath\n */\nCypress.Commands.add(\"validateSwaggerFile\", (url, savePath) => {\n\tcy.task('log', `validateSwaggerFile: ${url} -- ${savePath}`)\n\t\t.then(() => {\n\t\t\treturn cy\n\t\t\t\t.request(url)\n\t\t\t\t.then((response) => cy.writeFile(savePath, response.body, { log: false }))\n\t\t\t\t.then(() => cy.exec(`yarn swagger-lint '${savePath}'`, { failOnNonZeroExit: false }))\n\t\t\t\t.then((result) => cy.task('log', `Swagger Vacuum Results:\\n${result.stdout || ''}`)\n\t\t\t\t\t.then(() => expect(result.exitCode).to.eq(0)));\n\t\t});\n});\n\n/**\n * Check the swagger schema for a specific endpoint:\n *\n * @param {string}  method        API Method in swagger doc, \"get\", \"put\", \"post\", \"delete\"\n * @param {integer} code          Swagger doc endpoint response code, exactly as defined in swagger doc\n * @param {string}  path          Swagger doc endpoint path, exactly as defined in swagger doc\n * @param {*}       data          The API response data to check against the swagger schema\n */\nCypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => {\n\tcy.task('validateSwaggerSchema', {\n\t\tfile:           Cypress.env('swaggerBase'),\n\t\tendpoint:       path,\n\t\tmethod:         method,\n\t\tstatusCode:     code,\n\t\tresponseSchema: data,\n\t\tverbose:        true\n\t}).should('equal', null);\n});\n\nCypress.Commands.add('createInitialUser', (defaultUser) => {\n\tlet user = {\n\t\tname:        'Cypress McGee',\n\t\tnickname:    'Cypress',\n\t\temail:       'cypress@example.com',\n\t\tauth:        {\n\t\t\ttype:   'password',\n\t\t\tsecret: 'changeme'\n\t\t},\n\t};\n\n\tif (typeof defaultUser === 'object' && defaultUser) {\n\t\tuser = Object.assign({}, user, defaultUser);\n\t}\n\n\treturn cy.task('backendApiPost', {\n\t\tpath: '/api/users',\n\t\tdata: user,\n\t}).then((data) => {\n\t\t// Check the swagger schema:\n\t\tcy.validateSwaggerSchema('post', 201, '/users', data);\n\t\texpect(data).to.have.property('id');\n\t\texpect(data.id).to.be.greaterThan(0);\n\t\tcy.wrap(data);\n\t});\n});\n\nCypress.Commands.add('getToken', (defaultUser, defaultAuth) => {\n\tif (typeof defaultAuth === 'object' && defaultAuth) {\n\t\tif (!defaultUser) {\n\t\t\tdefaultUser = {};\n\t\t}\n\t\tdefaultUser.auth = defaultAuth;\n\t}\n\n\tcy.task('backendApiGet', {\n\t\tpath: '/api/',\n\t}).then((data) => {\n\t\t// Check the swagger schema:\n\t\tcy.validateSwaggerSchema('get', 200, '/', data);\n\n\t\tif (!data.setup) {\n\t\t\tcy.log('Setup = false');\n\t\t\t// create a new user\n\t\t\tcy.createInitialUser(defaultUser).then(() => {\n\t\t\t\treturn cy.getToken(defaultUser);\n\t\t\t});\n\t\t} else {\n\t\t\tlet auth = {\n\t\t\t\tidentity: 'cypress@example.com',\n\t\t\t\tsecret:   'changeme',\n\t\t\t};\n\n\t\t\tif (typeof defaultUser === 'object' && defaultUser && typeof defaultUser.auth === 'object' && defaultUser.auth) {\n\t\t\t\tauth = Object.assign({}, auth, defaultUser.auth);\n\t\t\t}\n\n\t\t\tcy.log('Setup = true');\n\t\t\t// login with existing user\n\t\t\tcy.task('backendApiPost', {\n\t\t\t\tpath: '/api/tokens',\n\t\t\t\tdata: auth,\n\t\t\t}).then((res) => {\n\t\t\t\tcy.wrap(res.token);\n\t\t\t});\n\t\t}\n\t});\n});\n\nCypress.Commands.add('resetUsers', () => {\n\tcy.task('backendApiDelete', {\n\t\tpath: '/api/users'\n\t}).then((data) => {\n\t\texpect(data).to.be.equal(true);\n\t\tcy.wrap(data);\n\t});\n});\n\n// TODO: copied from v3, is this usable?\nCypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeout = 60) => {\n\tcy.log(`Waiting for certificate (${certID}) status (${expected}) timeout (${timeout})`);\n\n\tcy.waitUntil(() => cy.task('backendApiGet', {\n\t\ttoken: token,\n\t\tpath:  `/api/certificates/${certID}`\n\t}).then((data) => {\n\t\treturn data.result.status === expected;\n\t}), {\n\t\terrorMsg: 'Waiting for certificate status failed',\n\t\ttimeout:  timeout * 1000,\n\t\tinterval: 5000\n\t});\n});\n"
  },
  {
    "path": "test/cypress/support/e2e.js",
    "content": "import './commands.mjs';\n\nCypress.on('uncaught:exception', (/*err, runnable*/) => {\n\t// returning false here prevents Cypress from\n\t// failing the test\n\treturn false;\n});\n"
  },
  {
    "path": "test/jsconfig.json",
    "content": "{\n\t\"include\": [\n\t\t\"./node_modules/cypress\",\n\t\t\"cypress/**/*.js\",\n\t\t\"cypress/config/dev.mjs\",\n\t\t\"cypress/config/ci.mjs\",\n\t\t\"cypress/plugins/index.mjs\",\n\t\t\"cypress/plugins/backendApi/task.mjs\",\n\t\t\"cypress/plugins/backendApi/logger.mjs\",\n\t\t\"cypress/plugins/backendApi/client.mjs\",\n\t\t\"cypress/support/commands.mjs\"\n\t]\n}\n"
  },
  {
    "path": "test/multi-reporter.json",
    "content": "{\n\t\"reporterEnabled\": \"spec, mocha-junit-reporter\",\n\t\"mochaJunitReporterReporterOptions\": {\n\t\t\"jenkinsMode\": true,\n\t\t\"rootSuiteTitle\": \"Cypress.npm\",\n\t\t\"jenkinsClassnamePrefix\": \"Cypress.npm.\",\n\t\t\"mochaFile\": \"results/junit/cypress.npm.[hash].xml\"\n\t}\n}\n"
  },
  {
    "path": "test/package.json",
    "content": "{\n\t\"name\": \"npm-test\",\n\t\"version\": \"1.0.0\",\n\t\"description\": \"\",\n\t\"main\": \"index.js\",\n\t\"dependencies\": {\n\t\t\"@jc21/cypress-swagger-validation\": \"^0.3.2\",\n\t\t\"@quobix/vacuum\": \"^0.24.0\",\n\t\t\"axios\": \"^1.13.6\",\n\t\t\"chalk\": \"^5.6.2\",\n\t\t\"cypress\": \"^15.11.0\",\n\t\t\"cypress-multi-reporters\": \"^2.0.5\",\n\t\t\"cypress-wait-until\": \"^3.0.2\",\n\t\t\"eslint\": \"^10.0.2\",\n\t\t\"eslint-plugin-align-assignments\": \"^1.1.2\",\n\t\t\"eslint-plugin-chai-friendly\": \"^1.1.0\",\n\t\t\"eslint-plugin-cypress\": \"^6.1.0\",\n\t\t\"form-data\": \"^4.0.5\",\n\t\t\"lodash\": \"^4.17.23\",\n\t\t\"mocha\": \"^11.7.5\",\n\t\t\"mocha-junit-reporter\": \"^2.2.1\"\n\t},\n\t\"scripts\": {\n\t\t\"cypress\": \"HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/ci.mjs\",\n\t\t\"cypress:headless\": \"HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/ci.mjs\",\n\t\t\"cypress:dev\": \"cypress run --config-file=cypress/config/dev.mjs\",\n\t\t\"swagger-lint\": \"vacuum lint -b -q -d -a --no-clip -n=warn\"\n\t},\n\t\"author\": \"\",\n\t\"license\": \"ISC\"\n}\n"
  },
  {
    "path": "test/vacuum-rules.yaml",
    "content": "description: Recommended rules for a high quality specification.\ndocumentationUrl: https://quobix.com/vacuum/rulesets/recommended\nrules:\n    component-description:\n        category:\n            description: Documentation is really important, in OpenAPI, just about everything can and should have a description. This set of rules checks for absent descriptions, poor quality descriptions (copy/paste), or short descriptions.\n            id: descriptions\n            name: Descriptions\n        description: Component description check\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Components are the inputs and outputs of a specification. A user needs to be able to understand each component and what id does. Descriptions are critical to understanding components. Add a description!\n        id: component-description\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasComponentDescriptions\n        type: validation\n    duplicate-paths:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Paths cannot be duplicated; only the last definition will be kept.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Duplicate path definitions found in your OpenAPI specification. In YAML, duplicate keys are allowed, but only the last occurrence is used. This means earlier path definitions are silently ignored, which can lead to missing API endpoints in your specification.\n        id: duplicate-paths\n        recommended: true\n        severity: error\n        then:\n            function: duplicatePaths\n        type: validation\n    duplicated-entry-in-enum:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: Enum values must not have duplicate entry\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Enums need to be unique, you can't duplicate them in the same definition. Please remove the duplicate value.\n        id: duplicated-entry-in-enum\n        recommended: true\n        severity: error\n        then:\n            function: duplicatedEnum\n        type: validation\n    info-description:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: Info section is missing a description\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: The 'info' section is missing a description, surely you want people to know what this spec is all about, right?\n        id: info-description\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: infoDescription\n        type: validation\n    info-license-spdx:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: License section cannot contain both an identifier and a URL, they are mutually exclusive.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: A license can contain either a URL or an SPDX identifier, but not both, They are mutually exclusive and cannot both be present. Choose one or the other\n        id: info-license-spdx\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: infoLicenseURLSPDX\n        type: validation\n    migrate-zally-ignore:\n        category:\n            description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications.\n            id: validation\n            name: Validation\n        description: x-zally-ignore keys should be migrated to x-lint-ignore for compatibility with vacuum\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Migrate x-zally-ignore directives to vacuum's x-lint-ignore. Rename the key to x-lint-ignore and update the ignored rule id to the vacuum equivalent rule.\n        id: migrate-zally-ignore\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: migrateZallyIgnore\n        type: validation\n    no-$ref-siblings:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: $ref values cannot be placed next to other properties (like a description)\n        formats:\n            - oas2\n            - oas3\n        given: $\n        howToFix: $ref values must not be placed next to sibling nodes, There should only be a single node  when using $ref. A common mistake is adding 'description' next to a $ref. This is wrong. remove all siblings!\n        id: no-$ref-siblings\n        recommended: true\n        severity: error\n        then:\n            function: refSiblings\n        type: validation\n    no-ambiguous-paths:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Paths need to resolve unambiguously from one another\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Paths must all resolve unambiguously, they can't be confused with one another (/{id}/ambiguous and /ambiguous/{id} are the same thing. Make sure every path and the variables used are unique and do conflict with one another. Check the ordering of variables and the naming of path segments.\n        id: no-ambiguous-paths\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: noAmbiguousPaths\n        type: validation\n    no-eval-in-markdown:\n        category:\n            description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications.\n            id: validation\n            name: Validation\n        description: Markdown descriptions must not have `eval()` statements'\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Remove all references to 'eval()' in the description. These can be used by malicious actors to embed code in contracts that is then executed when read by a browser.\n        id: no-eval-in-markdown\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: noEvalDescription\n            functionOptions:\n                pattern: eval\\(\n        type: validation\n    no-http-verbs-in-path:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path segments must not contain an HTTP verb\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: When HTTP verbs (get/post/put etc) are used in path segments, it muddies the semantics of REST and creates a confusing and inconsistent experience. It's highly recommended that verbs are not used in path segments. Replace those HTTP verbs with more meaningful nouns.\n        id: no-http-verbs-in-path\n        recommended: true\n        severity: warn\n        then:\n            function: noVerbsInPath\n        type: style\n    no-request-body:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: HTTP GET and DELETE should not accept request bodies\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Remove 'requestBody' from HTTP GET and DELETE methods\n        id: no-request-body\n        recommended: true\n        severity: warn\n        then:\n            function: noRequestBody\n        type: style\n    no-script-tags-in-markdown:\n        category:\n            description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications.\n            id: validation\n            name: Validation\n        description: Markdown descriptions must not have `<script>` tags'\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Remove all references to '<script>' tags from the description. These can be used by malicious actors to load remote code if the spec is being parsed by a browser.\n        id: no-script-tags-in-markdown\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: noEvalDescription\n            functionOptions:\n                pattern: <script\n        type: validation\n    no-unnecessary-combinator:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: Schema combinators (allOf, anyOf, oneOf) with only one item should be replaced with the item directly.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Schema combinators (allOf, anyOf, oneOf) with only a single item are unnecessary and should be replaced with the item directly for cleaner, more readable schemas.\n        id: no-unnecessary-combinator\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasUnnecessaryCombinator\n        type: style\n    oas-missing-type:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: All schemas and their properties should have a type field (unless using enum, const, or a composition)\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: 'Add a `type` field to all schemas and properties. Valid types are: string, number, integer, boolean, array, object, or null.'\n        id: oas-missing-type\n        recommended: true\n        severity: info\n        then:\n            function: missingType\n        type: validation\n    oas-schema-check:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: All document schemas must have a valid type defined\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Make sure each schema has a value type defined. Without a type, the schema is useless\n        id: oas-schema-check\n        recommended: true\n        severity: error\n        then:\n            function: schemaTypeCheck\n        type: validation\n    oas2-anyOf:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: '`anyOf` was introduced in OpenAPI 3.0, cannot be used un OpenAPI 2 specs'\n        formats:\n            - oas2\n        given: $\n        howToFix: You can't use 'anyOf' in Swagger/OpenAPI 2 specs. It was added in version 3. You have to remove it\n        id: oas2-anyOf\n        recommended: true\n        severity: error\n        then:\n            function: oasPolymorphicAnyOf\n        type: validation\n    oas2-api-host:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: OpenAPI `host` must be present and a non-empty string\n        formats:\n            - oas2\n        given: $\n        howToFix: The 'host' value is missing. How is a user supposed to know where the API actually lives? The host is critical in order for consumers to be able to call the API. Add an API host!\n        id: oas2-api-host\n        recommended: true\n        severity: info\n        then:\n            field: host\n            function: truthy\n        type: style\n    oas2-api-schemes:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: OpenAPI host `schemes` must be present and non-empty array\n        formats:\n            - oas2\n        given: $\n        howToFix: Add an array of supported host 'schemes' to the root of the specification. These are the available API schemes (like https/http).\n        id: oas2-api-schemes\n        recommended: true\n        severity: warn\n        then:\n            field: schemes\n            function: schema\n            functionOptions:\n                forceValidation: true\n                schema:\n                    items:\n                        minItems: 1\n                        type: string\n                    type: array\n                    uniqueItems: true\n        type: validation\n    oas2-discriminator:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: discriminators are used correctly in schemas\n        formats:\n            - oas2\n        given: $\n        howToFix: When using polymorphism, a discriminator should also be provided to allow tools to understand how to compose your models when generating code. Add a correct discriminator.\n        id: oas2-discriminator\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oasDiscriminator\n        type: validation\n    oas2-host-not-example:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: Host URL should not point at example.com\n        formats:\n            - oas2\n        given: $.host\n        howToFix: Remove 'example.com' from the host URL, it's not going to work.\n        id: oas2-host-not-example\n        recommended: true\n        severity: warn\n        then:\n            function: pattern\n            functionOptions:\n                notMatch: example\\.com\n        type: style\n    oas2-host-trailing-slash:\n        category:\n            description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.\n            id: information\n            name: Contract Information\n        description: Host URL should not contain a trailing slash\n        formats:\n            - oas2\n        given: $.host\n        howToFix: Remove the trailing slash from the host URL. This may cause some tools to incorrectly add a double slash to paths.\n        id: oas2-host-trailing-slash\n        recommended: true\n        severity: warn\n        then:\n            function: pattern\n            functionOptions:\n                notMatch: /$\n        type: style\n    oas2-oneOf:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: '`oneOf` was introduced in OpenAPI 3.0, cannot be used un OpenAPI 2 specs'\n        formats:\n            - oas2\n        given: $\n        howToFix: You can't use 'oneOf' in Swagger/OpenAPI 2 specs. It was added in version 3. You have to remove it\n        id: oas2-oneOf\n        recommended: true\n        severity: error\n        then:\n            function: oasPolymorphicOneOf\n        type: validation\n    oas2-operation-formData-consume-check:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: 'Operations with `in: formData` parameter must include `application/x-www-form-urlencoded` or `multipart/form-data` in their `consumes` property.'\n        formats:\n            - oas2\n        given: $\n        howToFix: When using 'formData', the parameter must include the correct mime-types. Make sure you use 'application/x-www-form-urlencoded' or 'multipart/form-data' as the 'consumes' value in your parameter.\n        id: oas2-operation-formData-consume-check\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasOpFormDataConsumeCheck\n        type: validation\n    oas2-operation-security-defined:\n        category:\n            description: Security plays a central role in RESTful APIs. These rules make sure that the correct definitions have been used and put in the right places.\n            id: security\n            name: Security\n        description: '`security` values must match a scheme defined in securityDefinitions'\n        formats:\n            - oas2\n        given: $\n        howToFix: When defining security definitions for operations, you need to ensure they match the globally defined security schemes. Check $.securityDefinitions to make sure your values align.\n        id: oas2-operation-security-defined\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oas2OpSecurityDefined\n        type: validation\n    oas2-parameter-description:\n        category:\n            description: Documentation is really important, in OpenAPI, just about everything can and should have a description. This set of rules checks for absent descriptions, poor quality descriptions (copy/paste), or short descriptions.\n            id: descriptions\n            name: Descriptions\n        description: Parameter description checks\n        formats:\n            - oas2\n        given: $\n        howToFix: All parameters should have a description. Descriptions are critical to understanding how an API works correctly. Please add a description to all parameters.\n        id: oas2-parameter-description\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasParamDescriptions\n        type: style\n    oas2-schema:\n        category:\n            description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications.\n            id: validation\n            name: Validation\n        description: OpenAPI 2 specification is invalid\n        formats:\n            - oas2\n        given: $\n        howToFix: The schema isn't valid Swagger/OpenAPI 2. Check the errors for more details\n        id: oas2-schema\n        recommended: true\n        severity: error\n        then:\n            function: oasDocumentSchema\n        type: validation\n    oas2-unused-definition:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: Check for unused definitions and bad references\n        formats:\n            - oas2\n        given: $\n        howToFix: Orphaned components are not used by anything. You might have plans to use them later, or they could be older schemas that never got cleaned up. A clean spec is a happy spec. Prune your orphaned components.\n        id: oas2-unused-definition\n        recommended: true\n        severity: warn\n        then:\n            function: oasUnusedComponent\n        type: validation\n    oas3-api-servers:\n        category:\n            description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications.\n            id: validation\n            name: Validation\n        description: Check for valid API servers definition\n        formats:\n            - oas3\n        given: $\n        howToFix: Ensure server URIs are correct and valid, check the schemes, ensure descriptions are complete.\n        id: oas3-api-servers\n        recommended: true\n        severity: warn\n        then:\n            function: oasAPIServers\n        type: validation\n    oas3-example-external-check:\n        category:\n            description: Examples help consumers understand how API calls should look. They are really important for automated tooling for mocking and testing. These rules check examples have been added to component schemas, parameters and operations. These rules also check that examples match the schema and types provided.\n            id: examples\n            name: Examples\n        description: Examples cannot use both `value` and `externalValue` together.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Examples are critical for consumers to be able to understand schemas and models defined by the spec. Without examples, developers can't understand the type of data the API will return in real life. Examples are turned into mocks and can provide a rich testing capability for APIs. Add detailed examples everywhere!\n        id: oas3-example-external-check\n        recommended: true\n        severity: warn\n        then:\n            function: oasExampleExternal\n        type: validation\n    oas3-missing-example:\n        category:\n            description: Examples help consumers understand how API calls should look. They are really important for automated tooling for mocking and testing. These rules check examples have been added to component schemas, parameters and operations. These rules also check that examples match the schema and types provided.\n            id: examples\n            name: Examples\n        description: Ensure everything that can have an example, contains one\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Examples are critical for consumers to be able to understand schemas and models defined by the spec. Without examples, developers can't understand the type of data the API will return in real life. Examples are turned into mocks and can provide a rich testing capability for APIs. Add detailed examples everywhere!\n        id: oas3-missing-example\n        recommended: true\n        severity: warn\n        then:\n            function: oasExampleMissing\n        type: validation\n    oas3-no-$ref-siblings:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: '`$ref` values cannot be placed next to other properties, except `description` and `summary`'\n        formats:\n            - oas3_1\n        given: $\n        howToFix: $ref values must not be placed next to sibling nodes, except `description` and `summary` nodes.  This is wrong. remove all additional siblings!\n        id: oas3-no-$ref-siblings\n        recommended: true\n        severity: error\n        then:\n            function: oasRefSiblings\n        type: validation\n    oas3-operation-security-defined:\n        category:\n            description: Security plays a central role in RESTful APIs. These rules make sure that the correct definitions have been used and put in the right places.\n            id: security\n            name: Security\n        description: '`security` values must match a scheme defined in components.securitySchemes'\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: When defining security values for operations, you need to ensure they match the globally defined security schemes. Check $.components.securitySchemes to make sure your values align.\n        id: oas3-operation-security-defined\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oasOpSecurityDefined\n            functionOptions:\n                schemesPath: $.components.securitySchemes\n        type: validation\n    oas3-parameter-description:\n        category:\n            description: Documentation is really important, in OpenAPI, just about everything can and should have a description. This set of rules checks for absent descriptions, poor quality descriptions (copy/paste), or short descriptions.\n            id: descriptions\n            name: Descriptions\n        description: Parameter description checks\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: All parameters should have a description. Descriptions are critical to understanding how an API works correctly. Please add a description to all parameters.\n        id: oas3-parameter-description\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasParamDescriptions\n        type: style\n    oas3-schema:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: OpenAPI 3+ specification is invalid\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: The schema isn't valid OpenAPI 3. Check the errors for more details\n        id: oas3-schema\n        recommended: true\n        severity: error\n        then:\n            function: oasDocumentSchema\n        type: validation\n    oas3-unused-component:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: Check for unused components and bad references\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Unused components / definitions are generally the result of the OpenAPI contract being updated without considering references. Another reference could have been updated, or an operation changed that no longer references this component. Remove this component from the spec, or re-link to it from another component or operation to fix the problem.\n        id: oas3-unused-component\n        recommended: true\n        severity: warn\n        then:\n            function: oasUnusedComponent\n        type: validation\n    oas3-valid-schema-example:\n        category:\n            description: Examples help consumers understand how API calls should look. They are really important for automated tooling for mocking and testing. These rules check examples have been added to component schemas, parameters and operations. These rules also check that examples match the schema and types provided.\n            id: examples\n            name: Examples\n        description: If an example has been used, check the schema is valid\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Examples are critical for consumers to be able to understand schemas and models defined by the spec. Without examples, developers can't understand the type of data the API will return in real life. Examples are turned into mocks and can provide a rich testing capability for APIs. Add detailed examples everywhere!\n        id: oas3-valid-schema-example\n        recommended: true\n        severity: warn\n        then:\n            function: oasExampleSchema\n        type: validation\n    operation-description:\n        category:\n            description: Documentation is really important, in OpenAPI, just about everything can and should have a description. This set of rules checks for absent descriptions, poor quality descriptions (copy/paste), or short descriptions.\n            id: descriptions\n            name: Descriptions\n        description: Operation description checks\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: All operations must have a description. Descriptions explain how the operation works, and how users should use it and what to expect. Operation descriptions make up the bulk of API documentation. so please, add a description!\n        id: operation-description\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasDescriptions\n            functionOptions:\n                minWords: \"1\"\n        type: validation\n    operation-operationId:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Every operation must contain an `operationId`.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Every single operation needs an operationId. It's a critical requirement to be able to identify each individual operation uniquely. Please add an operationId to the operation.\n        id: operation-operationId\n        recommended: true\n        severity: error\n        then:\n            function: oasOpId\n        type: validation\n    operation-operationId-unique:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Every operation must have unique `operationId`.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths\n        howToFix: An operationId needs to be unique, there can't be any duplicates in the document, you can't re-use them. Make sure the ID used for this operation is unique.\n        id: operation-operationId-unique\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oasOpIdUnique\n        type: validation\n    operation-operationId-valid-in-url:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: OperationId must use URL friendly characters\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths[*][*]\n        howToFix: An operationId is critical to correct code generation and operation identification. The operationId should really be designed in a way to make it friendly when used as part of a URL. Remove non-standard URL characters.\n        id: operation-operationId-valid-in-url\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            field: operationId\n            function: pattern\n            functionOptions:\n                match: ^[A-Za-z0-9-._~:/?#\\[\\]@!\\$&'()*+,;=]*$\n        type: validation\n    operation-parameters:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Operation parameters are unique and non-repeating.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths\n        howToFix: Make sure that all the operation parameters are unique and non-repeating, don't duplicate names, don'tre-use parameter names in the same operation.\n        id: operation-parameters\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oasOpParams\n        type: validation\n    operation-success-response:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Operation must have at least one `2xx` or a `3xx` response.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Make sure that your operation returns a 'success' response via  2xx or 3xx response code. An API consumer will always expect a success response\n        id: operation-success-response\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            field: responses\n            function: oasOpSuccessResponse\n        type: style\n    operation-tag-defined:\n        category:\n            description: Tags are used as meta-data for operations. They are mainly used by tooling as a taxonomy mechanism to build navigation, search and more. Tags are important as they help consumers navigate the contract when using documentation, testing, code generation or analysis tools.\n            id: tags\n            name: Tags\n        description: Operation tags must be defined in global tags.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: This tag has not been defined in the global scope, you should always ensure that any tags used in operations, are defined globally in the root 'tags' definition.\n        id: operation-tag-defined\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasTagDefined\n        type: validation\n    operation-tags:\n        category:\n            description: Tags are used as meta-data for operations. They are mainly used by tooling as a taxonomy mechanism to build navigation, search and more. Tags are important as they help consumers navigate the contract when using documentation, testing, code generation or analysis tools.\n            id: tags\n            name: Tags\n        description: Operation `tags` are missing/empty\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Operations use tags to define the domain(s) they are apart of. Generally a single tag per operation is used, however some tools use multiple tags. The point is that you need tags! Add some tags to the operation that match the globally available ones.\n        id: operation-tags\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: oasOperationTags\n        type: validation\n    path-declarations-must-exist:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path parameter declarations must not be empty ex. `/api/{}` is invalid\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths\n        howToFix: Paths define the endpoint for operations. Without paths, there is no API. You need to add 'paths' to the root of the specification.\n        id: path-declarations-must-exist\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: pattern\n            functionOptions:\n                notMatch: '{}'\n        type: validation\n    path-item-refs:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path items should not use references ($ref) values. They are technically not allowed.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Path items should not use references for defining operations. It's technically allowed, but not great style\n        id: path-item-refs\n        recommended: true\n        severity: info\n        then:\n            function: pathItemReferences\n        type: validation\n    path-keys-no-trailing-slash:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path must not end with a slash\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths\n        howToFix: Paths should not end with a trailing slash, it can confuse tooling and isn't valid as a path Remove the trailing slash from the path.\n        id: path-keys-no-trailing-slash\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: pattern\n            functionOptions:\n                notMatch: .+\\/$\n        type: validation\n    path-not-include-query:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path must not include query string\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $.paths\n        howToFix: Query strings are defined as parameters for an operation, they should not be included in the path Please remove it and correctly define as a parameter.\n        id: path-not-include-query\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: pattern\n            functionOptions:\n                notMatch: \\?\n        type: validation\n    path-params:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path parameters must be defined and valid.\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n        given: $\n        howToFix: Path parameters need to match up with the parameters defined for the path, or in an operation that sits under that path. Make sure variable names match up and are defined correctly.\n        id: path-params\n        recommended: true\n        resolved: true\n        severity: error\n        then:\n            function: oasPathParam\n        type: validation\n    paths-kebab-case:\n        category:\n            description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth.\n            id: operations\n            name: Operations\n        description: Path segments must only use kebab-case (no underscores or uppercase)\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Path segments should not contain any uppercase letters, punctuation or underscores. The only valid way to separate words in a segment, is to use a hyphen '-'. The elements that are violating the rule are highlighted in the violation description. These are the elements that need to change.\n        id: paths-kebab-case\n        recommended: true\n        severity: warn\n        then:\n            function: pathsKebabCase\n        type: validation\n    typed-enum:\n        category:\n            description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures.\n            id: schemas\n            name: Schemas\n        description: Enum values must respect the specified type\n        formats:\n            - oas3\n            - oas3_1\n            - oas3_2\n            - oas2\n        given: $\n        howToFix: Enum values lock down the number of variable inputs a parameter or schema can have. The problem here is that the Enum defined, does not match the specified type. Fix the type!\n        id: typed-enum\n        recommended: true\n        resolved: true\n        severity: warn\n        then:\n            function: typedEnum\n        type: validation\n"
  },
  {
    "path": "test/vacuum.conf.yaml",
    "content": "ruleset: \"./vacuum-rules.yaml\"\n"
  }
]