Repository: NginxProxyManager/nginx-proxy-manager
Branch: develop
Commit: 15896132ff4b
Files: 789
Total size: 1.5 MB
Directory structure:
gitextract_e35dww2e/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── dns_challenge_request.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ └── workflows/
│ └── stale.yml
├── .gitignore
├── .version
├── LICENSE
├── README.md
├── backend/
│ ├── .gitignore
│ ├── app.js
│ ├── biome.json
│ ├── certbot/
│ │ ├── README.md
│ │ └── dns-plugins.json
│ ├── config/
│ │ ├── README.md
│ │ ├── default.json
│ │ └── sqlite-test-db.json
│ ├── db.js
│ ├── index.js
│ ├── internal/
│ │ ├── 2fa.js
│ │ ├── access-list.js
│ │ ├── audit-log.js
│ │ ├── certificate.js
│ │ ├── dead-host.js
│ │ ├── host.js
│ │ ├── ip_ranges.js
│ │ ├── nginx.js
│ │ ├── proxy-host.js
│ │ ├── redirection-host.js
│ │ ├── remote-version.js
│ │ ├── report.js
│ │ ├── setting.js
│ │ ├── stream.js
│ │ ├── token.js
│ │ └── user.js
│ ├── knexfile.js
│ ├── lib/
│ │ ├── access/
│ │ │ ├── access_lists-create.json
│ │ │ ├── access_lists-delete.json
│ │ │ ├── access_lists-get.json
│ │ │ ├── access_lists-list.json
│ │ │ ├── access_lists-update.json
│ │ │ ├── auditlog-list.json
│ │ │ ├── certificates-create.json
│ │ │ ├── certificates-delete.json
│ │ │ ├── certificates-get.json
│ │ │ ├── certificates-list.json
│ │ │ ├── certificates-update.json
│ │ │ ├── dead_hosts-create.json
│ │ │ ├── dead_hosts-delete.json
│ │ │ ├── dead_hosts-get.json
│ │ │ ├── dead_hosts-list.json
│ │ │ ├── dead_hosts-update.json
│ │ │ ├── permissions.json
│ │ │ ├── proxy_hosts-create.json
│ │ │ ├── proxy_hosts-delete.json
│ │ │ ├── proxy_hosts-get.json
│ │ │ ├── proxy_hosts-list.json
│ │ │ ├── proxy_hosts-update.json
│ │ │ ├── redirection_hosts-create.json
│ │ │ ├── redirection_hosts-delete.json
│ │ │ ├── redirection_hosts-get.json
│ │ │ ├── redirection_hosts-list.json
│ │ │ ├── redirection_hosts-update.json
│ │ │ ├── reports-hosts.json
│ │ │ ├── roles.json
│ │ │ ├── settings-get.json
│ │ │ ├── settings-list.json
│ │ │ ├── settings-update.json
│ │ │ ├── streams-create.json
│ │ │ ├── streams-delete.json
│ │ │ ├── streams-get.json
│ │ │ ├── streams-list.json
│ │ │ ├── streams-update.json
│ │ │ ├── users-create.json
│ │ │ ├── users-delete.json
│ │ │ ├── users-get.json
│ │ │ ├── users-list.json
│ │ │ ├── users-loginas.json
│ │ │ ├── users-password.json
│ │ │ ├── users-permissions.json
│ │ │ └── users-update.json
│ │ ├── access.js
│ │ ├── certbot.js
│ │ ├── config.js
│ │ ├── error.js
│ │ ├── express/
│ │ │ ├── cors.js
│ │ │ ├── jwt-decode.js
│ │ │ ├── jwt.js
│ │ │ ├── pagination.js
│ │ │ └── user-id-from-me.js
│ │ ├── helpers.js
│ │ ├── migrate_template.js
│ │ ├── utils.js
│ │ └── validator/
│ │ ├── api.js
│ │ └── index.js
│ ├── logger.js
│ ├── migrate.js
│ ├── migrations/
│ │ ├── 20180618015850_initial.js
│ │ ├── 20180929054513_websockets.js
│ │ ├── 20181019052346_forward_host.js
│ │ ├── 20181113041458_http2_support.js
│ │ ├── 20181213013211_forward_scheme.js
│ │ ├── 20190104035154_disabled.js
│ │ ├── 20190215115310_customlocations.js
│ │ ├── 20190218060101_hsts.js
│ │ ├── 20190227065017_settings.js
│ │ ├── 20200410143839_access_list_client.js
│ │ ├── 20200410143840_access_list_client_fix.js
│ │ ├── 20201014143841_pass_auth.js
│ │ ├── 20210210154702_redirection_scheme.js
│ │ ├── 20210210154703_redirection_status_code.js
│ │ ├── 20210423103500_stream_domain.js
│ │ ├── 20211108145214_regenerate_default_host.js
│ │ ├── 20240427161436_stream_ssl.js
│ │ ├── 20251111090000_redirect_auto_scheme.js
│ │ └── 20260131163528_trust_forwarded_proto.js
│ ├── models/
│ │ ├── access_list.js
│ │ ├── access_list_auth.js
│ │ ├── access_list_client.js
│ │ ├── audit-log.js
│ │ ├── auth.js
│ │ ├── certificate.js
│ │ ├── dead_host.js
│ │ ├── now_helper.js
│ │ ├── proxy_host.js
│ │ ├── redirection_host.js
│ │ ├── setting.js
│ │ ├── stream.js
│ │ ├── token.js
│ │ ├── user.js
│ │ └── user_permission.js
│ ├── nodemon.json
│ ├── package.json
│ ├── routes/
│ │ ├── audit-log.js
│ │ ├── main.js
│ │ ├── nginx/
│ │ │ ├── access_lists.js
│ │ │ ├── certificates.js
│ │ │ ├── dead_hosts.js
│ │ │ ├── proxy_hosts.js
│ │ │ ├── redirection_hosts.js
│ │ │ └── streams.js
│ │ ├── reports.js
│ │ ├── schema.js
│ │ ├── settings.js
│ │ ├── tokens.js
│ │ ├── users.js
│ │ └── version.js
│ ├── schema/
│ │ ├── common.json
│ │ ├── components/
│ │ │ ├── access-list-object.json
│ │ │ ├── audit-log-list.json
│ │ │ ├── audit-log-object.json
│ │ │ ├── certificate-list.json
│ │ │ ├── certificate-object.json
│ │ │ ├── check-version-object.json
│ │ │ ├── dead-host-list.json
│ │ │ ├── dead-host-object.json
│ │ │ ├── dns-providers-list.json
│ │ │ ├── error-object.json
│ │ │ ├── error.json
│ │ │ ├── health-object.json
│ │ │ ├── permission-object.json
│ │ │ ├── proxy-host-list.json
│ │ │ ├── proxy-host-object.json
│ │ │ ├── redirection-host-list.json
│ │ │ ├── redirection-host-object.json
│ │ │ ├── security-schemes.json
│ │ │ ├── setting-list.json
│ │ │ ├── setting-object.json
│ │ │ ├── stream-list.json
│ │ │ ├── stream-object.json
│ │ │ ├── token-challenge.json
│ │ │ ├── token-object.json
│ │ │ ├── user-list.json
│ │ │ └── user-object.json
│ │ ├── index.js
│ │ ├── paths/
│ │ │ ├── audit-log/
│ │ │ │ ├── get.json
│ │ │ │ └── id/
│ │ │ │ └── get.json
│ │ │ ├── get.json
│ │ │ ├── nginx/
│ │ │ │ ├── access-lists/
│ │ │ │ │ ├── get.json
│ │ │ │ │ ├── listID/
│ │ │ │ │ │ ├── delete.json
│ │ │ │ │ │ ├── get.json
│ │ │ │ │ │ └── put.json
│ │ │ │ │ └── post.json
│ │ │ │ ├── certificates/
│ │ │ │ │ ├── certID/
│ │ │ │ │ │ ├── delete.json
│ │ │ │ │ │ ├── download/
│ │ │ │ │ │ │ └── get.json
│ │ │ │ │ │ ├── get.json
│ │ │ │ │ │ ├── renew/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ └── upload/
│ │ │ │ │ │ └── post.json
│ │ │ │ │ ├── dns-providers/
│ │ │ │ │ │ └── get.json
│ │ │ │ │ ├── get.json
│ │ │ │ │ ├── post.json
│ │ │ │ │ ├── test-http/
│ │ │ │ │ │ └── post.json
│ │ │ │ │ └── validate/
│ │ │ │ │ └── post.json
│ │ │ │ ├── dead-hosts/
│ │ │ │ │ ├── get.json
│ │ │ │ │ ├── hostID/
│ │ │ │ │ │ ├── delete.json
│ │ │ │ │ │ ├── disable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── enable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── get.json
│ │ │ │ │ │ └── put.json
│ │ │ │ │ └── post.json
│ │ │ │ ├── proxy-hosts/
│ │ │ │ │ ├── get.json
│ │ │ │ │ ├── hostID/
│ │ │ │ │ │ ├── delete.json
│ │ │ │ │ │ ├── disable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── enable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── get.json
│ │ │ │ │ │ └── put.json
│ │ │ │ │ └── post.json
│ │ │ │ ├── redirection-hosts/
│ │ │ │ │ ├── get.json
│ │ │ │ │ ├── hostID/
│ │ │ │ │ │ ├── delete.json
│ │ │ │ │ │ ├── disable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── enable/
│ │ │ │ │ │ │ └── post.json
│ │ │ │ │ │ ├── get.json
│ │ │ │ │ │ └── put.json
│ │ │ │ │ └── post.json
│ │ │ │ └── streams/
│ │ │ │ ├── get.json
│ │ │ │ ├── post.json
│ │ │ │ └── streamID/
│ │ │ │ ├── delete.json
│ │ │ │ ├── disable/
│ │ │ │ │ └── post.json
│ │ │ │ ├── enable/
│ │ │ │ │ └── post.json
│ │ │ │ ├── get.json
│ │ │ │ └── put.json
│ │ │ ├── reports/
│ │ │ │ └── hosts/
│ │ │ │ └── get.json
│ │ │ ├── schema/
│ │ │ │ └── get.json
│ │ │ ├── settings/
│ │ │ │ ├── get.json
│ │ │ │ └── settingID/
│ │ │ │ ├── get.json
│ │ │ │ └── put.json
│ │ │ ├── tokens/
│ │ │ │ ├── 2fa/
│ │ │ │ │ └── post.json
│ │ │ │ ├── get.json
│ │ │ │ └── post.json
│ │ │ ├── users/
│ │ │ │ ├── get.json
│ │ │ │ ├── post.json
│ │ │ │ └── userID/
│ │ │ │ ├── 2fa/
│ │ │ │ │ ├── backup-codes/
│ │ │ │ │ │ └── post.json
│ │ │ │ │ ├── delete.json
│ │ │ │ │ ├── enable/
│ │ │ │ │ │ └── post.json
│ │ │ │ │ ├── get.json
│ │ │ │ │ └── post.json
│ │ │ │ ├── auth/
│ │ │ │ │ └── put.json
│ │ │ │ ├── delete.json
│ │ │ │ ├── get.json
│ │ │ │ ├── login/
│ │ │ │ │ └── post.json
│ │ │ │ ├── permissions/
│ │ │ │ │ └── put.json
│ │ │ │ └── put.json
│ │ │ └── version/
│ │ │ └── check/
│ │ │ └── get.json
│ │ └── swagger.json
│ ├── scripts/
│ │ ├── install-certbot-plugins
│ │ └── regenerate-config
│ ├── setup.js
│ ├── templates/
│ │ ├── _access.conf
│ │ ├── _assets.conf
│ │ ├── _certificates.conf
│ │ ├── _certificates_stream.conf
│ │ ├── _exploits.conf
│ │ ├── _forced_ssl.conf
│ │ ├── _header_comment.conf
│ │ ├── _hsts.conf
│ │ ├── _hsts_map.conf
│ │ ├── _listen.conf
│ │ ├── _location.conf
│ │ ├── dead_host.conf
│ │ ├── default.conf
│ │ ├── ip_ranges.conf
│ │ ├── letsencrypt-request.conf
│ │ ├── proxy_host.conf
│ │ ├── redirection_host.conf
│ │ └── stream.conf
│ └── validate-schema.js
├── docker/
│ ├── .dive-ci
│ ├── Dockerfile
│ ├── ci.env
│ ├── dev/
│ │ ├── Dockerfile
│ │ ├── dnsrouter-config.json
│ │ ├── letsencrypt.ini
│ │ ├── pdns-db.sql
│ │ └── squid.conf
│ ├── docker-compose.ci.mysql.yml
│ ├── docker-compose.ci.postgres.yml
│ ├── docker-compose.ci.sqlite.yml
│ ├── docker-compose.ci.yml
│ ├── docker-compose.dev.yml
│ ├── rootfs/
│ │ ├── etc/
│ │ │ ├── letsencrypt.ini
│ │ │ ├── logrotate.d/
│ │ │ │ └── nginx-proxy-manager
│ │ │ ├── nginx/
│ │ │ │ ├── conf.d/
│ │ │ │ │ ├── default.conf
│ │ │ │ │ ├── dev.conf
│ │ │ │ │ ├── include/
│ │ │ │ │ │ ├── .gitignore
│ │ │ │ │ │ ├── assets.conf
│ │ │ │ │ │ ├── block-exploits.conf
│ │ │ │ │ │ ├── force-ssl.conf
│ │ │ │ │ │ ├── ip_ranges.conf
│ │ │ │ │ │ ├── letsencrypt-acme-challenge.conf
│ │ │ │ │ │ ├── log-proxy.conf
│ │ │ │ │ │ ├── log-stream.conf
│ │ │ │ │ │ ├── proxy.conf
│ │ │ │ │ │ ├── ssl-cache-stream.conf
│ │ │ │ │ │ ├── ssl-cache.conf
│ │ │ │ │ │ └── ssl-ciphers.conf
│ │ │ │ │ └── production.conf
│ │ │ │ ├── mime.types
│ │ │ │ └── nginx.conf
│ │ │ └── s6-overlay/
│ │ │ └── s6-rc.d/
│ │ │ ├── backend/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── prepare
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ ├── frontend/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── prepare
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ ├── nginx/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── prepare
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ ├── prepare/
│ │ │ │ ├── 00-all.sh
│ │ │ │ ├── 10-usergroup.sh
│ │ │ │ ├── 20-paths.sh
│ │ │ │ ├── 30-ownership.sh
│ │ │ │ ├── 40-dynamic.sh
│ │ │ │ ├── 50-ipv6.sh
│ │ │ │ ├── 60-secrets.sh
│ │ │ │ ├── 90-banner.sh
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── base
│ │ │ │ ├── type
│ │ │ │ └── up
│ │ │ └── user/
│ │ │ └── contents.d/
│ │ │ ├── backend
│ │ │ ├── frontend
│ │ │ ├── nginx
│ │ │ └── prepare
│ │ ├── root/
│ │ │ └── .bashrc
│ │ ├── usr/
│ │ │ └── bin/
│ │ │ ├── check-health
│ │ │ └── common.sh
│ │ └── var/
│ │ └── www/
│ │ └── html/
│ │ └── index.html
│ └── scripts/
│ └── install-s6
├── docs/
│ ├── .gitignore
│ ├── .vitepress/
│ │ ├── config.mts
│ │ └── theme/
│ │ ├── custom.css
│ │ └── index.ts
│ ├── package.json
│ ├── scripts/
│ │ └── set-version.sh
│ └── src/
│ ├── advanced-config/
│ │ └── index.md
│ ├── faq/
│ │ └── index.md
│ ├── guide/
│ │ └── index.md
│ ├── index.md
│ ├── public/
│ │ └── robots.txt
│ ├── screenshots/
│ │ └── index.md
│ ├── setup/
│ │ └── index.md
│ ├── third-party/
│ │ └── index.md
│ └── upgrading/
│ └── index.md
├── frontend/
│ ├── .gitignore
│ ├── biome.json
│ ├── check-locales.cjs
│ ├── index.html
│ ├── package.json
│ ├── public/
│ │ └── images/
│ │ └── favicon/
│ │ ├── browserconfig.xml
│ │ └── site.webmanifest
│ ├── src/
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── Router.tsx
│ │ ├── api/
│ │ │ └── backend/
│ │ │ ├── base.ts
│ │ │ ├── checkVersion.ts
│ │ │ ├── createAccessList.ts
│ │ │ ├── createCertificate.ts
│ │ │ ├── createDeadHost.ts
│ │ │ ├── createProxyHost.ts
│ │ │ ├── createRedirectionHost.ts
│ │ │ ├── createStream.ts
│ │ │ ├── createUser.ts
│ │ │ ├── deleteAccessList.ts
│ │ │ ├── deleteCertificate.ts
│ │ │ ├── deleteDeadHost.ts
│ │ │ ├── deleteProxyHost.ts
│ │ │ ├── deleteRedirectionHost.ts
│ │ │ ├── deleteStream.ts
│ │ │ ├── deleteUser.ts
│ │ │ ├── downloadCertificate.ts
│ │ │ ├── expansions.ts
│ │ │ ├── getAccessList.ts
│ │ │ ├── getAccessLists.ts
│ │ │ ├── getAuditLog.ts
│ │ │ ├── getAuditLogs.ts
│ │ │ ├── getCertificate.ts
│ │ │ ├── getCertificateDNSProviders.ts
│ │ │ ├── getCertificates.ts
│ │ │ ├── getDeadHost.ts
│ │ │ ├── getDeadHosts.ts
│ │ │ ├── getHealth.ts
│ │ │ ├── getHostsReport.ts
│ │ │ ├── getProxyHost.ts
│ │ │ ├── getProxyHosts.ts
│ │ │ ├── getRedirectionHost.ts
│ │ │ ├── getRedirectionHosts.ts
│ │ │ ├── getSetting.ts
│ │ │ ├── getSettings.ts
│ │ │ ├── getStream.ts
│ │ │ ├── getStreams.ts
│ │ │ ├── getToken.ts
│ │ │ ├── getUser.ts
│ │ │ ├── getUsers.ts
│ │ │ ├── helpers.ts
│ │ │ ├── index.ts
│ │ │ ├── loginAsUser.ts
│ │ │ ├── models.ts
│ │ │ ├── refreshToken.ts
│ │ │ ├── renewCertificate.ts
│ │ │ ├── responseTypes.ts
│ │ │ ├── setPermissions.ts
│ │ │ ├── testHttpCertificate.ts
│ │ │ ├── toggleDeadHost.ts
│ │ │ ├── toggleProxyHost.ts
│ │ │ ├── toggleRedirectionHost.ts
│ │ │ ├── toggleStream.ts
│ │ │ ├── toggleUser.ts
│ │ │ ├── twoFactor.ts
│ │ │ ├── updateAccessList.ts
│ │ │ ├── updateAuth.ts
│ │ │ ├── updateDeadHost.ts
│ │ │ ├── updateProxyHost.ts
│ │ │ ├── updateRedirectionHost.ts
│ │ │ ├── updateSetting.ts
│ │ │ ├── updateStream.ts
│ │ │ ├── updateUser.ts
│ │ │ ├── uploadCertificate.ts
│ │ │ └── validateCertificate.ts
│ │ ├── components/
│ │ │ ├── Button.tsx
│ │ │ ├── EmptyData.tsx
│ │ │ ├── ErrorNotFound.tsx
│ │ │ ├── Flag.tsx
│ │ │ ├── Form/
│ │ │ │ ├── AccessClientFields.tsx
│ │ │ │ ├── AccessField.tsx
│ │ │ │ ├── BasicAuthFields.tsx
│ │ │ │ ├── DNSProviderFields.module.css
│ │ │ │ ├── DNSProviderFields.tsx
│ │ │ │ ├── DomainNamesField.tsx
│ │ │ │ ├── LocationsFields.module.css
│ │ │ │ ├── LocationsFields.tsx
│ │ │ │ ├── NginxConfigField.tsx
│ │ │ │ ├── SSLCertificateField.tsx
│ │ │ │ ├── SSLOptionsFields.tsx
│ │ │ │ └── index.ts
│ │ │ ├── HasPermission.tsx
│ │ │ ├── Loading.module.css
│ │ │ ├── Loading.tsx
│ │ │ ├── LoadingPage.tsx
│ │ │ ├── LocalePicker.module.css
│ │ │ ├── LocalePicker.tsx
│ │ │ ├── NavLink.tsx
│ │ │ ├── Page.module.css
│ │ │ ├── Page.tsx
│ │ │ ├── SiteContainer.tsx
│ │ │ ├── SiteFooter.tsx
│ │ │ ├── SiteHeader.module.css
│ │ │ ├── SiteHeader.tsx
│ │ │ ├── SiteMenu.tsx
│ │ │ ├── Table/
│ │ │ │ ├── EmptyRow.tsx
│ │ │ │ ├── Formatter/
│ │ │ │ │ ├── AccessListformatter.tsx
│ │ │ │ │ ├── CertificateFormatter.tsx
│ │ │ │ │ ├── CertificateInUseFormatter.tsx
│ │ │ │ │ ├── DateFormatter.tsx
│ │ │ │ │ ├── DomainsFormatter.tsx
│ │ │ │ │ ├── EmailFormatter.tsx
│ │ │ │ │ ├── EventFormatter.tsx
│ │ │ │ │ ├── GravatarFormatter.tsx
│ │ │ │ │ ├── RolesFormatter.tsx
│ │ │ │ │ ├── TrueFalseFormatter.tsx
│ │ │ │ │ ├── ValueWithDateFormatter.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── TableBody.tsx
│ │ │ │ ├── TableHeader.tsx
│ │ │ │ ├── TableHelpers.ts
│ │ │ │ ├── TableLayout.tsx
│ │ │ │ └── index.ts
│ │ │ ├── ThemeSwitcher.module.css
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ ├── Unhealthy.tsx
│ │ │ └── index.ts
│ │ ├── context/
│ │ │ ├── AuthContext.tsx
│ │ │ ├── LocaleContext.tsx
│ │ │ ├── ThemeContext.tsx
│ │ │ └── index.ts
│ │ ├── declarations.d.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── useAccessList.ts
│ │ │ ├── useAccessLists.ts
│ │ │ ├── useAuditLog.ts
│ │ │ ├── useAuditLogs.ts
│ │ │ ├── useCertificate.ts
│ │ │ ├── useCertificates.ts
│ │ │ ├── useCheckVersion.ts
│ │ │ ├── useDeadHost.ts
│ │ │ ├── useDeadHosts.ts
│ │ │ ├── useDnsProviders.ts
│ │ │ ├── useHealth.ts
│ │ │ ├── useHostReport.ts
│ │ │ ├── useProxyHost.ts
│ │ │ ├── useProxyHosts.ts
│ │ │ ├── useRedirectionHost.ts
│ │ │ ├── useRedirectionHosts.ts
│ │ │ ├── useSetting.ts
│ │ │ ├── useStream.ts
│ │ │ ├── useStreams.ts
│ │ │ ├── useTheme.ts
│ │ │ ├── useUser.ts
│ │ │ └── useUsers.ts
│ │ ├── locale/
│ │ │ ├── IntlProvider.tsx
│ │ │ ├── README.md
│ │ │ ├── Utils.test.tsx
│ │ │ ├── Utils.ts
│ │ │ ├── index.ts
│ │ │ ├── scripts/
│ │ │ │ ├── locale-sort.cjs
│ │ │ │ └── locale-sort.sh
│ │ │ └── src/
│ │ │ ├── HelpDoc/
│ │ │ │ ├── bg/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── cs/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── de/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── en/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── es/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── et/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── fr/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ga/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── hu/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── id/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── it/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ja/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ko/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── nl/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── no/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── pl/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── pt/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ru/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── sk/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── tr/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ ├── vi/
│ │ │ │ │ ├── AccessLists.md
│ │ │ │ │ ├── Certificates.md
│ │ │ │ │ ├── DeadHosts.md
│ │ │ │ │ ├── ProxyHosts.md
│ │ │ │ │ ├── RedirectionHosts.md
│ │ │ │ │ ├── Streams.md
│ │ │ │ │ └── index.ts
│ │ │ │ └── zh/
│ │ │ │ ├── AccessLists.md
│ │ │ │ ├── Certificates.md
│ │ │ │ ├── DeadHosts.md
│ │ │ │ ├── ProxyHosts.md
│ │ │ │ ├── RedirectionHosts.md
│ │ │ │ ├── Streams.md
│ │ │ │ └── index.ts
│ │ │ ├── bg.json
│ │ │ ├── cs.json
│ │ │ ├── de.json
│ │ │ ├── en.json
│ │ │ ├── es.json
│ │ │ ├── et.json
│ │ │ ├── fr.json
│ │ │ ├── ga.json
│ │ │ ├── hu.json
│ │ │ ├── id.json
│ │ │ ├── it.json
│ │ │ ├── ja.json
│ │ │ ├── ko.json
│ │ │ ├── lang-list.json
│ │ │ ├── nl.json
│ │ │ ├── no.json
│ │ │ ├── pl.json
│ │ │ ├── pt.json
│ │ │ ├── ru.json
│ │ │ ├── sk.json
│ │ │ ├── tr.json
│ │ │ ├── vi.json
│ │ │ └── zh.json
│ │ ├── main.tsx
│ │ ├── modals/
│ │ │ ├── AccessListModal.tsx
│ │ │ ├── ChangePasswordModal.tsx
│ │ │ ├── CustomCertificateModal.tsx
│ │ │ ├── DNSCertificateModal.tsx
│ │ │ ├── DeadHostModal.tsx
│ │ │ ├── DeleteConfirmModal.tsx
│ │ │ ├── EventDetailsModal.tsx
│ │ │ ├── HTTPCertificateModal.tsx
│ │ │ ├── HelpModal.tsx
│ │ │ ├── PermissionsModal.module.css
│ │ │ ├── PermissionsModal.tsx
│ │ │ ├── ProxyHostModal.tsx
│ │ │ ├── RedirectionHostModal.tsx
│ │ │ ├── RenewCertificateModal.tsx
│ │ │ ├── SetPasswordModal.tsx
│ │ │ ├── StreamModal.tsx
│ │ │ ├── TwoFactorModal.tsx
│ │ │ ├── UserModal.tsx
│ │ │ └── index.ts
│ │ ├── modules/
│ │ │ ├── AuthStore.ts
│ │ │ ├── Permissions.ts
│ │ │ └── Validations.tsx
│ │ ├── notifications/
│ │ │ ├── Msg.module.css
│ │ │ ├── Msg.tsx
│ │ │ ├── helpers.tsx
│ │ │ └── index.ts
│ │ ├── pages/
│ │ │ ├── Access/
│ │ │ │ ├── Table.tsx
│ │ │ │ ├── TableWrapper.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── AuditLog/
│ │ │ │ ├── Table.tsx
│ │ │ │ ├── TableWrapper.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Certificates/
│ │ │ │ ├── Table.tsx
│ │ │ │ ├── TableWrapper.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Dashboard/
│ │ │ │ └── index.tsx
│ │ │ ├── Login/
│ │ │ │ ├── index.module.css
│ │ │ │ └── index.tsx
│ │ │ ├── Nginx/
│ │ │ │ ├── DeadHosts/
│ │ │ │ │ ├── Table.tsx
│ │ │ │ │ ├── TableWrapper.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── ProxyHosts/
│ │ │ │ │ ├── Table.tsx
│ │ │ │ │ ├── TableWrapper.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── RedirectionHosts/
│ │ │ │ │ ├── Table.tsx
│ │ │ │ │ ├── TableWrapper.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── Streams/
│ │ │ │ ├── Table.tsx
│ │ │ │ ├── TableWrapper.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Settings/
│ │ │ │ ├── DefaultSite.tsx
│ │ │ │ ├── Layout.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Setup/
│ │ │ │ ├── index.module.css
│ │ │ │ └── index.tsx
│ │ │ └── Users/
│ │ │ ├── Table.tsx
│ │ │ ├── TableWrapper.tsx
│ │ │ └── index.tsx
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── vitest-setup.js
├── scripts/
│ ├── .common.sh
│ ├── buildx
│ ├── ci/
│ │ ├── frontend-build
│ │ ├── fulltest-cypress
│ │ └── test-and-build
│ ├── cypress-dev
│ ├── destroy-dev
│ ├── docs-build
│ ├── docs-upload
│ ├── start-dev
│ ├── stop-dev
│ └── wait-healthy
└── test/
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── README.md
├── cypress/
│ ├── Dockerfile
│ ├── config/
│ │ ├── ci.mjs
│ │ └── dev.mjs
│ ├── e2e/
│ │ └── api/
│ │ ├── Certificates.cy.js
│ │ ├── Dashboard.cy.js
│ │ ├── FullCertProvision.cy.js
│ │ ├── Health.cy.js
│ │ ├── Ldap.cy.js
│ │ ├── OAuth.cy.js
│ │ ├── ProxyHosts.cy.js
│ │ ├── Settings.cy.js
│ │ ├── Streams.cy.js
│ │ ├── SwaggerSchema.cy.js
│ │ └── Users.cy.js
│ ├── fixtures/
│ │ ├── test.example.com-key.pem
│ │ └── test.example.com.pem
│ ├── plugins/
│ │ ├── backendApi/
│ │ │ ├── client.mjs
│ │ │ ├── logger.mjs
│ │ │ └── task.mjs
│ │ └── index.mjs
│ └── support/
│ ├── commands.mjs
│ └── e2e.js
├── jsconfig.json
├── multi-reporter.json
├── package.json
├── vacuum-rules.yaml
└── vacuum.conf.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Checklist**
- Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image?
- Yes / No
- Are you sure you're not using someone else's docker image?
- Yes / No
- Have you searched for similar issues (both open and closed)?
- Yes / No
**Describe the bug**
**Nginx Proxy Manager Version**
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
**Screenshots**
**Operating System**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/dns_challenge_request.md
================================================
---
name: DNS challenge provider request
about: Suggest a new provider to be available for a certificate DNS challenge
title: ''
labels: dns provider request
assignees: ''
---
**What provider would you like to see added to NPM?**
**Have you checked if a certbot plugin exists?**
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
**Describe the solution you'd like**
**Describe alternatives you've considered**
**Additional context**
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/backend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/test"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
groups:
updates:
update-types:
- "patch"
- "minor"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:'
stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:'
close-issue-message: 'Issue was closed due to inactivity.'
close-pr-message: 'PR was closed due to inactivity.'
days-before-stale: 182
days-before-close: 365
operations-per-run: 50
================================================
FILE: .gitignore
================================================
.DS_Store
.idea
.qodo
._*
.vscode
certbot-help.txt
test/node_modules
*/node_modules
docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf
================================================
FILE: .version
================================================
2.14.0
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup)
- [Full Setup](https://nginxproxymanager.com/setup/)
- [Screenshots](https://nginxproxymanager.com/screenshots/)
## Project Goal
I created this project to fill a personal need to provide users with an easy way to accomplish reverse
proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.
While there might be advanced options they are optional and the project should be as simple as possible
so that the barrier for entry here is low.
## Features
- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/)
- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx
- Free SSL using Let's Encrypt or provide your own custom SSL certificates
- Access Lists and basic HTTP Authentication for your hosts
- Advanced Nginx configuration available for super users
- User management, permissions and audit log
::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
use the `2.13.7` image tag if this applies to you.
:::
## Hosting your home network
I won't go in to too much detail here but here are the basics for someone new to this self-hosted world.
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like
- DuckDNS
- [Amazon Route53](https://github.com/jc21/route53-ddns)
- [Cloudflare](https://github.com/jc21/cloudflare-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. [Install Docker](https://docs.docker.com/install/)
2. Create a docker-compose.yml file similar to this:
```yml
services:
app:
image: 'docker.io/jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.
3. Bring up your stack by running
```bash
docker compose up -d
```
4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface.
Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81)
## Contributing
All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch.
CI is used in this project. All PR's must pass before being considered. After passing,
docker builds for PR's are available on dockerhub for manual verifications.
Documentation within the `develop` branch is available for preview at
[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com)
### Contributors
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).
## Getting Support
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)
3. [Reddit](https://reddit.com/r/nginxproxymanager)
================================================
FILE: backend/.gitignore
================================================
config/development.json
data/*
yarn-error.log
tmp
certbot.log
node_modules
core.*
================================================
FILE: backend/app.js
================================================
import bodyParser from "body-parser";
import compression from "compression";
import express from "express";
import fileUpload from "express-fileupload";
import { isDebugMode } from "./lib/config.js";
import cors from "./lib/express/cors.js";
import jwt from "./lib/express/jwt.js";
import { debug, express as logger } from "./logger.js";
import mainRoutes from "./routes/main.js";
/**
* App
*/
const app = express();
app.use(fileUpload());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Gzip
app.use(compression());
/**
* General Logging, BEFORE routes
*/
app.disable("x-powered-by");
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
app.enable("strict routing");
// pretty print JSON when not live
if (isDebugMode()) {
app.set("json spaces", 2);
}
// CORS for everything
app.use(cors);
// General security/cache related headers + server header
app.use((_, res, next) => {
let x_frame_options = "DENY";
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) {
x_frame_options = process.env.X_FRAME_OPTIONS;
}
res.set({
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": x_frame_options,
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
Pragma: "no-cache",
Expires: 0,
});
next();
});
app.use(jwt());
app.use("/", mainRoutes);
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, _) => {
const payload = {
error: {
code: err.status,
message: err.public ? err.message : "Internal Error",
},
};
if (typeof err.message_i18n !== "undefined") {
payload.error.message_i18n = err.message_i18n;
}
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
payload.debug = {
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
previous: err.previous,
};
}
// Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== "undefined" && err.stack) {
debug(logger, err.stack);
if (typeof err.public === "undefined" || !err.public) {
logger.warn(err.message);
}
}
res.status(err.status || 500).send(payload);
});
export default app;
================================================
FILE: backend/biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"!**/dist/**/*"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"formatWithErrors": true
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":BUN:",
":NODE:",
[
"npm:*",
"npm:*/**"
],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
[
"/src/*",
"/src/**"
],
[
"/**"
],
[
"#*",
"#*/**"
],
":PATH:"
]
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useUniqueElementIds": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"performance": {
"noDelete": "off"
},
"nursery": "off",
"a11y": {
"useSemanticElements": "off",
"useValidAnchor": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
}
================================================
FILE: backend/certbot/README.md
================================================
# Certbot dns-plugins
This file contains info about available Certbot DNS plugins.
This only works for plugins which use the standard argument structure, so:
--authenticator ---credentials ---propagation-seconds
File Structure:
```json
{
"cloudflare": {
"display_name": "Name displayed to the user",
"package_name": "Package name in PyPi repo",
"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)",
"dependencies": "Additional dependencies, space separated (as you would pass it to pip install)",
"credentials": "Template of the credentials file",
"full_plugin_name": "The full plugin name as used in the commandline with certbot, e.g. 'dns-njalla'"
},
...
}
```
================================================
FILE: backend/certbot/dns-plugins.json
================================================
{
"acmedns": {
"name": "ACME-DNS",
"package_name": "certbot-dns-acmedns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json",
"full_plugin_name": "dns-acmedns"
},
"active24": {
"name": "Active24",
"package_name": "certbot-dns-active24",
"version": "~=2.0.0",
"dependencies": "",
"credentials": "dns_active24_api_key = \ndns_active24_secret = ",
"full_plugin_name": "dns-active24"
},
"aliyun": {
"name": "Aliyun",
"package_name": "certbot-dns-aliyun",
"version": "~=2.0.0",
"dependencies": "",
"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef",
"full_plugin_name": "dns-aliyun"
},
"arvan": {
"name": "ArvanCloud",
"package_name": "certbot-dns-arvan",
"version": ">=0.1.0",
"dependencies": "",
"credentials": "dns_arvan_key = Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"full_plugin_name": "dns-arvan"
},
"azure": {
"name": "Azure",
"package_name": "certbot-dns-azure",
"version": "~=2.6.1",
"dependencies": "azure-mgmt-dns==8.2.0",
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
"full_plugin_name": "dns-azure"
},
"baidu": {
"name": "baidu",
"package_name": "certbot-dns-baidu",
"version": "~=0.1.1",
"dependencies": "",
"credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef",
"full_plugin_name": "dns-baidu"
},
"beget": {
"name":"Beget",
"package_name": "certbot-beget-plugin",
"version": "~=1.0.0.dev9",
"dependencies": "",
"credentials": "# Beget API credentials used by Certbot\nbeget_plugin_username = username\nbeget_plugin_password = password",
"full_plugin_name": "beget-plugin"
},
"bunny": {
"name": "bunny.net",
"package_name": "certbot-dns-bunny",
"version": "~=0.0.9",
"dependencies": "",
"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",
"full_plugin_name": "dns-bunny"
},
"cdmon": {
"name": "cdmon",
"package_name": "certbot-dns-cdmon",
"version": "~=0.4.1",
"dependencies": "",
"credentials": "dns_cdmon_api_key=your-cdmon-api-token\ndns_cdmon_domain=your_domain_is_optional",
"full_plugin_name": "dns-cdmon"
},
"cloudflare": {
"name": "Cloudflare",
"package_name": "certbot-dns-cloudflare",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-cloudflare"
},
"cloudns": {
"name": "ClouDNS",
"package_name": "certbot-dns-cloudns",
"version": "~=0.7.0",
"dependencies": "",
"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",
"full_plugin_name": "dns-cloudns"
},
"cloudxns": {
"name": "CloudXNS",
"package_name": "certbot-dns-cloudxns",
"version": "~=1.32.0",
"dependencies": "",
"credentials": "dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef\ndns_cloudxns_secret_key = 1122334455667788",
"full_plugin_name": "dns-cloudxns"
},
"constellix": {
"name": "Constellix",
"package_name": "certbot-dns-constellix",
"version": "~=0.2.1",
"dependencies": "",
"credentials": "dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595\ndns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b\ndns_constellix_endpoint = https://api.dns.constellix.com/v1",
"full_plugin_name": "dns-constellix"
},
"corenetworks": {
"name": "Core Networks",
"package_name": "certbot-dns-corenetworks",
"version": "~=0.1.4",
"dependencies": "",
"credentials": "dns_corenetworks_username = asaHB12r\ndns_corenetworks_password = secure_password",
"full_plugin_name": "dns-corenetworks"
},
"cpanel": {
"name": "cPanel",
"package_name": "certbot-dns-cpanel",
"version": "~=0.4.0",
"dependencies": "",
"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = your_username\ncpanel_password = your_password\ncpanel_token = your_api_token",
"full_plugin_name": "cpanel"
},
"ddnss": {
"name": "DDNSS",
"package_name": "certbot-dns-ddnss",
"version": "~=1.1.0",
"dependencies": "",
"credentials": "dns_ddnss_token = YOUR_DDNSS_API_TOKEN",
"full_plugin_name": "dns-ddnss"
},
"desec": {
"name": "deSEC",
"package_name": "certbot-dns-desec",
"version": "~=1.2.1",
"dependencies": "",
"credentials": "dns_desec_token = YOUR_DESEC_API_TOKEN\ndns_desec_endpoint = https://desec.io/api/v1/",
"full_plugin_name": "dns-desec"
},
"duckdns": {
"name": "DuckDNS",
"package_name": "certbot-dns-duckdns",
"version": "~=1.0",
"dependencies": "",
"credentials": "dns_duckdns_token=your-duckdns-token",
"full_plugin_name": "dns-duckdns"
},
"digitalocean": {
"name": "DigitalOcean",
"package_name": "certbot-dns-digitalocean",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
"full_plugin_name": "dns-digitalocean"
},
"directadmin": {
"name": "DirectAdmin",
"package_name": "certbot-dns-directadmin",
"version": "~=0.0.23",
"dependencies": "",
"credentials": "directadmin_url = https://my.directadminserver.com:2222\ndirectadmin_username = username\ndirectadmin_password = aSuperStrongPassword",
"full_plugin_name": "directadmin"
},
"dnsimple": {
"name": "DNSimple",
"package_name": "certbot-dns-dnsimple",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw",
"full_plugin_name": "dns-dnsimple"
},
"dnsmadeeasy": {
"name": "DNS Made Easy",
"package_name": "certbot-dns-dnsmadeeasy",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\ndns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55",
"full_plugin_name": "dns-dnsmadeeasy"
},
"dnsmulti": {
"name": "DnsMulti",
"package_name": "certbot-dns-multi",
"version": "~=4.9",
"dependencies": "",
"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",
"full_plugin_name": "dns-multi"
},
"dnspod": {
"name": "DNSPod",
"package_name": "certbot-dns-dnspod",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "dns_dnspod_email = \"email@example.com\"\ndns_dnspod_api_token = \"id,key\"",
"full_plugin_name": "dns-dnspod"
},
"domainoffensive": {
"name": "DomainOffensive (do.de)",
"package_name": "certbot-dns-domainoffensive",
"version": "~=2.0.0",
"dependencies": "",
"credentials": "dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN",
"full_plugin_name": "dns-domainoffensive"
},
"domeneshop": {
"name": "Domeneshop",
"package_name": "certbot-dns-domeneshop",
"version": "~=0.2.8",
"dependencies": "",
"credentials": "dns_domeneshop_client_token=YOUR_DOMENESHOP_CLIENT_TOKEN\ndns_domeneshop_client_secret=YOUR_DOMENESHOP_CLIENT_SECRET",
"full_plugin_name": "dns-domeneshop"
},
"dynu": {
"name": "Dynu",
"package_name": "certbot-dns-dynu",
"version": "~=0.0.1",
"dependencies": "",
"credentials": "dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN",
"full_plugin_name": "dns-dynu"
},
"easydns": {
"name": "easyDNS",
"package_name": "certbot-dns-easydns",
"version": "~=0.1.2",
"dependencies": "",
"credentials": "dns_easydns_usertoken = YOUR_EASYDNS_USERTOKEN\ndns_easydns_userkey = YOUR_EASYDNS_USERKEY\ndns_easydns_endpoint = https://rest.easydns.net",
"full_plugin_name": "dns-easydns"
},
"eurodns": {
"name": "EuroDNS",
"package_name": "certbot-dns-eurodns",
"version": "~=0.0.4",
"dependencies": "",
"credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy",
"full_plugin_name": "dns-eurodns"
},
"firstdomains": {
"name": "First Domains",
"package_name": "certbot-dns-firstdomains",
"version": ">=1.0",
"dependencies": "",
"credentials": "dns_firstdomains_username = myremoteuser\ndns_firstdomains_password = verysecureremoteuserpassword",
"full_plugin_name": "dns-firstdomains"
},
"freedns": {
"name": "FreeDNS",
"package_name": "certbot-dns-freedns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "dns_freedns_username = myremoteuser\ndns_freedns_password = verysecureremoteuserpassword",
"full_plugin_name": "dns-freedns"
},
"gandi": {
"name": "Gandi Live DNS",
"package_name": "certbot-dns-gandi",
"version": "~=1.6.1",
"dependencies": "",
"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
"full_plugin_name": "dns-gandi"
},
"gcore": {
"name": "Gcore DNS",
"package_name": "certbot-dns-gcore",
"version": "~=0.1.8",
"dependencies": "",
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-gcore"
},
"glesys": {
"name": "Glesys",
"package_name": "certbot-dns-glesys",
"version": "~=2.1.0",
"dependencies": "",
"credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue",
"full_plugin_name": "dns-glesys"
},
"godaddy": {
"name": "GoDaddy",
"package_name": "certbot-dns-godaddy",
"version": "==2.8.0",
"dependencies": "",
"credentials": "dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567\ndns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123",
"full_plugin_name": "dns-godaddy"
},
"google": {
"name": "Google",
"package_name": "certbot-dns-google",
"version": "=={{certbot-version}}",
"dependencies": "",
"credentials": "{\n\"type\": \"service_account\",\n...\n}",
"full_plugin_name": "dns-google"
},
"googledomains": {
"name": "GoogleDomainsDNS",
"package_name": "certbot-dns-google-domains",
"version": "~=0.1.5",
"dependencies": "",
"credentials": "dns_google_domains_access_token = 0123456789abcdef0123456789abcdef01234567\ndns_google_domains_zone = \"example.com\"",
"full_plugin_name": "dns-google-domains"
},
"he": {
"name": "Hurricane Electric",
"package_name": "certbot-dns-he",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
"full_plugin_name": "dns-he"
},
"he-ddns": {
"name": "Hurricane Electric - DDNS",
"package_name": "certbot-dns-he-ddns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "dns_he_ddns_password = verysecurepassword",
"full_plugin_name": "dns-he-ddns"
},
"hetzner": {
"name": "Hetzner",
"package_name": "certbot-dns-hetzner",
"version": "~=1.0.4",
"dependencies": "",
"credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-hetzner"
},
"hetzner-cloud": {
"name": "Hetzner Cloud",
"package_name": "certbot-dns-hetzner-cloud",
"version": "~=1.0.4",
"dependencies": "",
"credentials": "dns_hetzner_cloud_api_token = your_api_token_here",
"full_plugin_name": "dns-hetzner-cloud"
},
"hostingnl": {
"name": "Hosting.nl",
"package_name": "certbot-dns-hostingnl",
"version": "~=0.1.5",
"dependencies": "",
"credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-hostingnl"
},
"hover": {
"name": "Hover",
"package_name": "certbot-dns-hover",
"version": "~=1.2.1",
"dependencies": "",
"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",
"full_plugin_name": "dns-hover"
},
"infomaniak": {
"name": "Infomaniak",
"package_name": "certbot-dns-infomaniak",
"version": "~=0.2.2",
"dependencies": "",
"credentials": "dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"full_plugin_name": "dns-infomaniak"
},
"inwx": {
"name": "INWX",
"package_name": "certbot-dns-inwx",
"version": "~=2.1.2",
"dependencies": "",
"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",
"full_plugin_name": "dns-inwx"
},
"ionos": {
"name": "IONOS",
"package_name": "certbot-dns-ionos",
"version": "==2022.11.24",
"dependencies": "",
"credentials": "dns_ionos_prefix = myapikeyprefix\ndns_ionos_secret = verysecureapikeysecret\ndns_ionos_endpoint = https://api.hosting.ionos.com",
"full_plugin_name": "dns-ionos"
},
"ispconfig": {
"name": "ISPConfig",
"package_name": "certbot-dns-ispconfig",
"version": "~=0.2.0",
"dependencies": "",
"credentials": "dns_ispconfig_username = myremoteuser\ndns_ispconfig_password = verysecureremoteuserpassword\ndns_ispconfig_endpoint = https://localhost:8080",
"full_plugin_name": "dns-ispconfig"
},
"isset": {
"name": "Isset",
"package_name": "certbot-dns-isset",
"version": "~=0.0.3",
"dependencies": "",
"credentials": "dns_isset_endpoint=\"https://customer.isset.net/api\"\ndns_isset_token=\"\"",
"full_plugin_name": "dns-isset"
},
"joker": {
"name": "Joker",
"package_name": "certbot-dns-joker",
"version": "~=1.1.0",
"dependencies": "",
"credentials": "dns_joker_username = \ndns_joker_password = \ndns_joker_domain = ",
"full_plugin_name": "dns-joker"
},
"kas": {
"name": "All-Inkl",
"package_name": "certbot-dns-kas",
"version": "~=0.1.1",
"dependencies": "kasserver",
"credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password",
"full_plugin_name": "dns-kas"
},
"leaseweb": {
"name": "LeaseWeb",
"package_name": "certbot-dns-leaseweb",
"version": "~=1.0.3",
"dependencies": "",
"credentials": "dns_leaseweb_api_token = 01234556789",
"full_plugin_name": "dns-leaseweb"
},
"linode": {
"name": "Linode",
"package_name": "certbot-dns-linode",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\ndns_linode_version = [|3|4]",
"full_plugin_name": "dns-linode"
},
"loopia": {
"name": "Loopia",
"package_name": "certbot-dns-loopia",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_loopia_user = user@loopiaapi\ndns_loopia_password = abcdef0123456789abcdef01234567abcdef0123",
"full_plugin_name": "dns-loopia"
},
"luadns": {
"name": "LuaDNS",
"package_name": "certbot-dns-luadns",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-luadns"
},
"mchost24": {
"name": "MC-HOST24",
"package_name": "certbot-dns-mchost24",
"version": "",
"dependencies": "",
"credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=",
"full_plugin_name": "dns-mchost24"
},
"mijnhost": {
"name": "mijn.host",
"package_name": "certbot-dns-mijn-host",
"version": "~=0.0.4",
"dependencies": "",
"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-mijn-host"
},
"namecheap": {
"name": "Namecheap",
"package_name": "certbot-dns-namecheap",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_namecheap_username = 123456\ndns_namecheap_api_key = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-namecheap"
},
"netcup": {
"name": "netcup",
"package_name": "certbot-dns-netcup",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123",
"full_plugin_name": "dns-netcup"
},
"nicru": {
"name": "nic.ru",
"package_name": "certbot-dns-nicru",
"version": "~=1.0.3",
"dependencies": "",
"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",
"full_plugin_name": "dns-nicru"
},
"njalla": {
"name": "Njalla",
"package_name": "certbot-dns-njalla",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_njalla_token = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-njalla"
},
"nsone": {
"name": "NS1",
"package_name": "certbot-dns-nsone",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw",
"full_plugin_name": "dns-nsone"
},
"oci": {
"name": "Oracle Cloud Infrastructure DNS",
"package_name": "certbot-dns-oci",
"version": "~=0.3.6",
"dependencies": "oci",
"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",
"full_plugin_name": "dns-oci"
},
"ovh": {
"name": "OVH",
"package_name": "certbot-dns-ovh",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "dns_ovh_endpoint = ovh-eu\ndns_ovh_application_key = MDAwMDAwMDAwMDAw\ndns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\ndns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw",
"full_plugin_name": "dns-ovh"
},
"plesk": {
"name": "Plesk",
"package_name": "certbot-dns-plesk",
"version": "~=0.3.0",
"dependencies": "",
"credentials": "dns_plesk_username = your-username\ndns_plesk_password = secret\ndns_plesk_api_url = https://plesk-api-host:8443",
"full_plugin_name": "dns-plesk"
},
"porkbun": {
"name": "Porkbun",
"package_name": "certbot-dns-porkbun",
"version": "~=0.11.0",
"dependencies": "",
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
"full_plugin_name": "dns-porkbun"
},
"powerdns": {
"name": "PowerDNS",
"package_name": "certbot-dns-powerdns",
"version": "~=0.2.1",
"dependencies": "PyYAML==5.3.1",
"credentials": "dns_powerdns_api_url = https://api.mypowerdns.example.org\ndns_powerdns_api_key = AbCbASsd!@34",
"full_plugin_name": "dns-powerdns"
},
"regru": {
"name": "reg.ru",
"package_name": "certbot-regru",
"version": "~=1.0.2",
"dependencies": "",
"credentials": "dns_username=username\ndns_password=password",
"full_plugin_name": "dns"
},
"rfc2136": {
"name": "RFC 2136",
"package_name": "certbot-dns-rfc2136",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "# Target DNS server\ndns_rfc2136_server = 192.0.2.1\n# Target DNS port\ndns_rfc2136_port = 53\n# TSIG key name\ndns_rfc2136_name = keyname.\n# TSIG key secret\ndns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512",
"full_plugin_name": "dns-rfc2136"
},
"rockenstein": {
"name": "rockenstein AG",
"package_name": "certbot-dns-rockenstein",
"version": "~=1.0.0",
"dependencies": "",
"credentials": "dns_rockenstein_token=",
"full_plugin_name": "dns-rockenstein"
},
"route53": {
"name": "Route 53 (Amazon)",
"package_name": "certbot-dns-route53",
"version": "=={{certbot-version}}",
"dependencies": "acme=={{certbot-version}}",
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"full_plugin_name": "dns-route53"
},
"simply": {
"name": "Simply",
"package_name": "certbot-dns-simply",
"version": "~=0.1.2",
"dependencies": "",
"credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj",
"full_plugin_name": "dns-simply"
},
"spaceship": {
"name": "Spaceship",
"package_name": "certbot-dns-spaceship",
"version": "~=1.0.4",
"dependencies": "",
"credentials": "[spaceship]\napi_key=your_api_key\napi_secret=your_api_secret",
"full_plugin_name": "dns-spaceship"
},
"strato": {
"name": "Strato",
"package_name": "certbot-dns-strato",
"version": "~=0.2.2",
"dependencies": "",
"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\"",
"full_plugin_name": "dns-strato"
},
"selectelv2": {
"name": "Selectel api v2",
"package_name": "certbot-dns-selectel-api-v2",
"version": "~=0.3.0",
"dependencies": "",
"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",
"full_plugin_name": "dns-selectel-api-v2"
},
"timeweb": {
"name": "Timeweb Cloud",
"package_name": "certbot-dns-timeweb",
"version": "~=1.0.1",
"dependencies": "",
"credentials": "dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX",
"full_plugin_name": "dns-timeweb"
},
"transip": {
"name": "TransIP",
"package_name": "certbot-dns-transip",
"version": "~=0.5.2",
"dependencies": "",
"credentials": "dns_transip_username = my_username\ndns_transip_key_file = /etc/letsencrypt/transip-rsa.key",
"full_plugin_name": "dns-transip"
},
"tencentcloud": {
"name": "Tencent Cloud",
"package_name": "certbot-dns-tencentcloud",
"version": "~=2.0.2",
"dependencies": "",
"credentials": "dns_tencentcloud_secret_id = TENCENT_CLOUD_SECRET_ID\ndns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY",
"full_plugin_name": "dns-tencentcloud"
},
"vultr": {
"name": "Vultr",
"package_name": "certbot-dns-vultr",
"version": "~=1.1.0",
"dependencies": "",
"credentials": "dns_vultr_key = YOUR_VULTR_API_KEY",
"full_plugin_name": "dns-vultr"
},
"websupport": {
"name": "Websupport.sk",
"package_name": "certbot-dns-websupport",
"version": "~=2.0.1",
"dependencies": "",
"credentials": "dns_websupport_identifier = \ndns_websupport_secret_key = ",
"full_plugin_name": "dns-websupport"
},
"wedos": {
"name": "Wedos",
"package_name": "certbot-dns-wedos",
"version": "~=2.2",
"dependencies": "",
"credentials": "dns_wedos_user = \ndns_wedos_auth = ",
"full_plugin_name": "dns-wedos"
},
"edgedns": {
"name": "Akamai Edge DNS",
"package_name": "certbot-plugin-edgedns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf",
"full_plugin_name": "edgedns"
},
"zoneedit": {
"name": "ZoneEdit",
"package_name": "certbot-dns-zoneedit",
"version": "~=0.3.2",
"dependencies": "--no-deps dnspython",
"credentials": "dns_zoneedit_user = \ndns_zoneedit_token = ",
"full_plugin_name": "dns-zoneedit"
}
}
================================================
FILE: backend/config/README.md
================================================
These files are use in development and are not deployed as part of the final product.
================================================
FILE: backend/config/default.json
================================================
{
"database": {
"engine": "mysql2",
"host": "db",
"name": "npm",
"user": "npm",
"password": "npm",
"port": 3306
}
}
================================================
FILE: backend/config/sqlite-test-db.json
================================================
{
"database": {
"engine": "knex-native",
"knex": {
"client": "better-sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},
"pool": {
"min": 0,
"max": 1,
"createTimeoutMillis": 3000,
"acquireTimeoutMillis": 30000,
"idleTimeoutMillis": 30000,
"reapIntervalMillis": 1000,
"createRetryIntervalMillis": 100,
"propagateCreateError": false
},
"migrations": {
"tableName": "migrations",
"stub": "src/backend/lib/migrate_template.js",
"directory": "src/backend/migrations"
}
}
}
}
================================================
FILE: backend/db.js
================================================
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
let instance = null;
const generateDbConfig = () => {
if (!configHas("database")) {
throw new Error(
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
);
}
const cfg = configGet("database");
if (cfg.engine === "knex-native") {
return cfg.knex;
}
return {
client: cfg.engine,
connection: {
host: cfg.host,
user: cfg.user,
password: cfg.password,
database: cfg.name,
port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {})
},
migrations: {
tableName: "migrations",
},
};
};
const getInstance = () => {
if (!instance) {
instance = knex(generateDbConfig());
}
return instance;
}
export default getInstance;
================================================
FILE: backend/index.js
================================================
#!/usr/bin/env node
import app from "./app.js";
import internalCertificate from "./internal/certificate.js";
import internalIpRanges from "./internal/ip_ranges.js";
import { global as logger } from "./logger.js";
import { migrateUp } from "./migrate.js";
import { getCompiledSchema } from "./schema/index.js";
import setup from "./setup.js";
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false";
async function appStart() {
return migrateUp()
.then(setup)
.then(getCompiledSchema)
.then(() => {
if (!IP_RANGES_FETCH_ENABLED) {
logger.info("IP Ranges fetch is disabled by environment variable");
return;
}
logger.info("IP Ranges fetch is enabled");
return internalIpRanges.fetch().catch((err) => {
logger.error("IP Ranges fetch failed, continuing anyway:", err.message);
});
})
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
const server = app.listen(3000, () => {
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
process.on("SIGTERM", () => {
logger.info(`PID ${process.pid} received SIGTERM`);
server.close(() => {
logger.info("Stopping.");
process.exit(0);
});
});
});
})
.catch((err) => {
logger.error(`Startup Error: ${err.message}`, err);
setTimeout(appStart, 1000);
});
}
try {
appStart();
} catch (err) {
logger.fatal(err);
process.exit(1);
}
================================================
FILE: backend/internal/2fa.js
================================================
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { createGuardrails, generateSecret, generateURI, verify } from "otplib";
import errs from "../lib/error.js";
import authModel from "../models/auth.js";
import internalUser from "./user.js";
const APP_NAME = "Nginx Proxy Manager";
const BACKUP_CODE_COUNT = 8;
/**
* Generate backup codes
* @returns {Promise<{plain: string[], hashed: string[]}>}
*/
const generateBackupCodes = async () => {
const plain = [];
const hashed = [];
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
plain.push(code);
const hash = await bcrypt.hash(code, 10);
hashed.push(hash);
}
return { plain, hashed };
};
const internal2fa = {
/**
* Check if user has 2FA enabled
* @param {number} userId
* @returns {Promise}
*/
isEnabled: async (userId) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
return auth?.meta?.totp_enabled === true;
},
/**
* Get 2FA status for user
* @param {Access} access
* @param {number} userId
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
*/
getStatus: async (access, userId) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
let backup_codes_remaining = 0;
if (enabled) {
const backupCodes = auth.meta.backup_codes || [];
backup_codes_remaining = backupCodes.length;
}
return {
enabled,
backup_codes_remaining,
};
},
/**
* Start 2FA setup - store pending secret
*
* @param {Access} access
* @param {number} userId
* @returns {Promise<{secret: string, otpauth_url: string}>}
*/
startSetup: async (access, userId) => {
await access.can("users:password", userId);
const user = await internalUser.get(access, { id: userId });
const secret = generateSecret();
const otpauth_url = generateURI({
issuer: APP_NAME,
label: user.email,
secret: secret,
});
const auth = await internal2fa.getUserPasswordAuth(userId);
// ensure user isn't already setup for 2fa
const enabled = auth?.meta?.totp_enabled === true;
if (enabled) {
throw new errs.ValidationError("2FA is already enabled");
}
const meta = auth.meta || {};
meta.totp_pending_secret = secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { secret, otpauth_url };
},
/**
* Enable 2FA after verifying code
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<{backup_codes: string[]}>}
*/
enable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_pending_secret || false;
if (!secret) {
throw new errs.ValidationError("No pending 2FA setup found");
}
const result = await verify({ token: code, secret });
if (!result.valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = {
...auth.meta,
totp_secret: secret,
totp_enabled: true,
totp_enabled_at: new Date().toISOString(),
backup_codes: hashed,
};
delete meta.totp_pending_secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
/**
* Disable 2FA
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise}
*/
disable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
const result = await verify({
token: code,
secret: auth.meta.totp_secret,
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
if (!result.valid) {
throw new errs.AuthError("Invalid verification code");
}
const meta = { ...auth.meta };
delete meta.totp_secret;
delete meta.totp_enabled;
delete meta.totp_enabled_at;
delete meta.backup_codes;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
},
/**
* Verify 2FA code for login
*
* @param {number} userId
* @param {string} token
* @returns {Promise}
*/
verifyForLogin: async (userId, token) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_secret || false;
if (!secret) {
return false;
}
// Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars
// and the backup codes are 8 chars.
if (token.length === 6) {
const result = await verify({
token,
secret,
// These guardrails lower the minimum length requirement for secrets.
// In v12 of otplib the default minimum length is 10 and in v13 it is 16.
// Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets
// so people won't be locked out when upgrading.
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
if (result.valid) {
return true;
}
}
// Try backup codes
const backupCodes = auth?.meta?.backup_codes || [];
for (let i = 0; i < backupCodes.length; i++) {
const match = await bcrypt.compare(token.toUpperCase(), backupCodes[i]);
if (match) {
// Remove used backup code
const updatedCodes = [...backupCodes];
updatedCodes.splice(i, 1);
const meta = { ...auth.meta, backup_codes: updatedCodes };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return true;
}
}
return false;
},
/**
* Regenerate backup codes
*
* @param {Access} access
* @param {number} userId
* @param {string} token
* @returns {Promise<{backup_codes: string[]}>}
*/
regenerateBackupCodes: async (access, userId, token) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
const secret = auth?.meta?.totp_secret || false;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
if (!secret) {
throw new errs.ValidationError("No 2FA secret found");
}
const result = await verify({
token,
secret,
});
if (!result.valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = { ...auth.meta, backup_codes: hashed };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
getUserPasswordAuth: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.andWhere("type", "password")
.first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth not found");
}
return auth;
},
};
export default internal2fa;
================================================
FILE: backend/internal/access-list.js
================================================
import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { access as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
import accessListClientModel from "../models/access_list_client.js";
import proxyHostModel from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalNginx from "./nginx.js";
const omissions = () => {
return ["is_deleted"];
};
const internalAccessList = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
await access.can("access_lists:create", data);
const row = await accessListModel
.query()
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1),
})
.then(utils.omitRow(omissions()));
data.id = row.id;
const promises = [];
// Items
data.items.map((item) => {
promises.push(
accessListAuthModel.query().insert({
access_list_id: row.id,
username: item.username,
password: item.password,
}),
);
return true;
});
// Clients
data.clients?.map((client) => {
promises.push(
accessListClientModel.query().insert({
access_list_id: row.id,
address: client.address,
directive: client.directive,
}),
);
return true;
});
await Promise.all(promises);
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
},
true // skip masking
);
// Audit log
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
await internalAccessList.build(freshRow);
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "access-list",
object_id: freshRow.id,
meta: internalAccessList.maskItems(data),
});
return internalAccessList.maskItems(freshRow);
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.name]
* @param {String} [data.items]
* @return {Promise}
*/
update: async (access, data) => {
await access.can("access_lists:update", data.id);
const row = await internalAccessList.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
// patch name if specified
if (typeof data.name !== "undefined" && data.name) {
await accessListModel.query().where({ id: data.id }).patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
// Check for items and add/update/remove them
if (typeof data.items !== "undefined" && data.items) {
const promises = [];
const itemsToKeep = [];
data.items.map((item) => {
if (item.password) {
promises.push(
accessListAuthModel.query().insert({
access_list_id: data.id,
username: item.username,
password: item.password,
}),
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
itemsToKeep.push(item.username);
}
return true;
});
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
if (itemsToKeep.length) {
query.andWhere("username", "NOT IN", itemsToKeep);
}
await query;
// Add new items
if (promises.length) {
await Promise.all(promises);
}
}
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const clientPromises = [];
data.clients.map((client) => {
if (client.address) {
clientPromises.push(
accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
}),
);
}
return true;
});
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
// Add new clitens
if (clientPromises.length) {
await Promise.all(clientPromises);
}
}
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "access-list",
object_id: data.id,
meta: internalAccessList.maskItems(data),
});
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true // skip masking
);
await internalAccessList.build(freshRow)
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skipMasking]
* @return {Promise}
*/
get: async (access, data, skipMasking) => {
const thisData = data || {};
const accessData = await access.can("access_lists:get", thisData.id)
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
})
.where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
let row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
if (!skipMasking && typeof row.items !== "undefined" && row.items) {
row = internalAccessList.maskItems(row);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("access_lists:delete", data.id);
const row = await internalAccessList.get(access, {
id: data.id,
expand: ["proxy_hosts", "items", "clients"],
});
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
await accessListModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
await proxyHostModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map((_val, idx) => {
row.proxy_hosts[idx].access_list_id = 0;
return true;
});
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
await internalNginx.reload();
// delete the htpasswd file
try {
fs.unlinkSync(internalAccessList.getFilename(row));
} catch (_err) {
// do nothing
}
// 4. audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "access-list",
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]),
});
return true;
},
/**
* All Lists
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("access_lists:list");
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
})
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients]")
.orderBy("access_list.name", "ASC");
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string") {
query.where(function () {
this.where("name", "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (rows) {
rows.map((row, idx) => {
if (typeof row.items !== "undefined" && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
return true;
});
}
return rows;
},
/**
* Count is used in reports
*
* @param {Integer} userId
* @param {String} visibility
* @returns {Promise}
*/
getCount: async (userId, visibility) => {
const query = accessListModel
.query()
.count("id as count")
.where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", userId);
}
const row = await query.first();
return Number.parseInt(row.count, 10);
},
/**
* @param {Object} list
* @returns {Object}
*/
maskItems: (list) => {
if (list && typeof list.items !== "undefined") {
list.items.map((val, idx) => {
let repeatFor = 8;
let firstChar = "*";
if (typeof val.password !== "undefined" && val.password) {
repeatFor = val.password.length - 1;
firstChar = val.password.charAt(0);
}
list.items[idx].hint = firstChar + "*".repeat(repeatFor);
list.items[idx].password = "";
return true;
});
}
return list;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @returns {String}
*/
getFilename: (list) => {
return `/data/access/${list.id}`;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @param {String} list.name
* @param {Array} list.items
* @returns {Promise}
*/
build: async (list) => {
logger.info(`Building Access file #${list.id} for: ${list.name}`);
const htpasswdFile = internalAccessList.getFilename(list);
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswdFile);
} catch (_err) {
// do nothing
}
// 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});
// 3. generate password for each user
if (list.items.length) {
await new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((_i, item, next) => {
if (item.password?.length) {
logger.info(`Adding: ${item.username}`);
utils.execFile('openssl', ['passwd', '-apr1', item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'});
} catch (err) {
reject(err);
}
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`);
resolve(results);
});
});
}
}
}
export default internalAccessList;
================================================
FILE: backend/internal/audit-log.js
================================================
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import auditLogModel from "../models/audit-log.js";
const internalAuditLog = {
/**
* All logs
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
await access.can("auditlog:list");
const query = auditLogModel
.query()
.orderBy("created_on", "DESC")
.orderBy("id", "DESC")
.limit(100)
.allowGraph("[user]");
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return await query;
},
/**
* @param {Access} access
* @param {Object} [data]
* @param {Integer} [data.id] Defaults to the token user
* @param {Array} [data.expand]
* @return {Promise}
*/
get: async (access, data) => {
await access.can("auditlog:list");
const query = auditLogModel
.query()
.andWhere("id", data.id)
.allowGraph("[user]")
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
const row = await query;
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return row;
},
/**
* This method should not be publicly used, it doesn't check certain things. It will be assumed
* that permission to add to audit log is already considered, however the access token is used for
* default user id determination.
*
* @param {Access} access
* @param {Object} data
* @param {String} data.action
* @param {Number} [data.user_id]
* @param {Number} [data.object_id]
* @param {Number} [data.object_type]
* @param {Object} [data.meta]
* @returns {Promise}
*/
add: async (access, data) => {
if (typeof data.user_id === "undefined" || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
if (typeof data.action === "undefined" || !data.action) {
throw new errs.InternalValidationError("Audit log entry must contain an Action");
}
// Make sure at least 1 of the IDs are set and action
return await auditLogModel.query().insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || "",
object_id: data.object_id || 0,
meta: data.meta || {},
});
},
};
export default internalAuditLog;
================================================
FILE: backend/internal/certificate.js
================================================
import fs from "node:fs";
import https from "node:https";
import path from "path";
import archiver from "archiver";
import _ from "lodash";
import moment from "moment";
import { ProxyAgent } from "proxy-agent";
import tempWrite from "temp-write";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
import { installPlugin } from "../lib/certbot.js";
import { useLetsencryptServer, useLetsencryptStaging } from "../lib/config.js";
import error from "../lib/error.js";
import utils from "../lib/utils.js";
import { debug, ssl as logger } from "../logger.js";
import certificateModel from "../models/certificate.js";
import tokenModel from "../models/token.js";
import userModel from "../models/user.js";
import internalAuditLog from "./audit-log.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const letsencryptConfig = "/etc/letsencrypt.ini";
const certbotCommand = "certbot";
const certbotLogsDir = "/data/logs";
const certbotWorkDir = "/tmp/letsencrypt-lib";
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"];
};
const internalCertificate = {
allowedSslFiles: ["certificate", "certificate_key", "intermediate_certificate"],
intervalTimeout: 1000 * 60 * 60, // 1 hour
interval: null,
intervalProcessing: false,
renewBeforeExpirationBy: [30, "days"],
initTimer: () => {
logger.info("Let's Encrypt Renewal Timer initialized");
internalCertificate.interval = setInterval(
internalCertificate.processExpiringHosts,
internalCertificate.intervalTimeout,
);
// And do this now as well
internalCertificate.processExpiringHosts();
},
/**
* Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required
*/
processExpiringHosts: () => {
if (!internalCertificate.intervalProcessing) {
internalCertificate.intervalProcessing = true;
logger.info(
`Renewing SSL certs expiring within ${internalCertificate.renewBeforeExpirationBy[0]} ${internalCertificate.renewBeforeExpirationBy[1]} ...`,
);
const expirationThreshold = moment()
.add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1])
.format("YYYY-MM-DD HH:mm:ss");
// Fetch all the letsencrypt certs from the db that will expire within the configured threshold
certificateModel
.query()
.where("is_deleted", 0)
.andWhere("provider", "letsencrypt")
.andWhere("expires_on", "<", expirationThreshold)
.then((certificates) => {
if (!certificates || !certificates.length) {
return null;
}
/**
* Renews must be run sequentially or we'll get an error 'Another
* instance of Certbot is already running.'
*/
let sequence = Promise.resolve();
certificates.forEach((certificate) => {
sequence = sequence.then(() =>
internalCertificate
.renew(
{
can: () =>
Promise.resolve({
permission_visibility: "all",
}),
token: tokenModel(),
},
{ id: certificate.id },
)
.catch((err) => {
// Don't want to stop the train here, just log the error
logger.error(err.message);
}),
);
});
return sequence;
})
.then(() => {
logger.info("Completed SSL cert renew process");
internalCertificate.intervalProcessing = false;
})
.catch((err) => {
logger.error(err);
internalCertificate.intervalProcessing = false;
});
}
},
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
await access.can("certificates:create", data);
data.owner_user_id = access.token.getUserId(1);
if (data.provider === "letsencrypt") {
data.nice_name = data.domain_names.join(", ");
}
// this command really should clean up and delete the cert if it can't fully succeed
const certificate = await certificateModel.query().insertAndFetch(data);
try {
if (certificate.provider === "letsencrypt") {
// Request a new Cert from LE. Let the fun begin.
// 1. Find out any hosts that are using any of the hostnames in this cert
// 2. Disable them in nginx temporarily
// 3. Generate the LE config
// 4. Request cert
// 5. Remove LE config
// 6. Re-instate previously disabled hosts
// 1. Find out any hosts that are using any of the hostnames in this cert
const inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names);
// 2. Disable them in nginx temporarily
await internalCertificate.disableInUseHosts(inUseResult);
const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first();
if (!user || !user.email) {
throw new error.ValidationError(
"A valid email address must be set on your user account to use Let's Encrypt",
);
}
// With DNS challenge no config is needed, so skip 3 and 5.
if (certificate.meta?.dns_challenge) {
try {
await internalNginx.reload();
// 4. Request cert
await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email);
await internalNginx.reload();
// 6. Re-instate previously disabled hosts
await internalCertificate.enableInUseHosts(inUseResult);
} catch (err) {
// In the event of failure, revert things and throw err back
await internalCertificate.enableInUseHosts(inUseResult);
await internalNginx.reload();
throw err;
}
} else {
// 3. Generate the LE config
try {
await internalNginx.generateLetsEncryptRequestConfig(certificate);
await internalNginx.reload();
setTimeout(() => {}, 5000);
// 4. Request cert
await internalCertificate.requestLetsEncryptSsl(certificate, user.email);
// 5. Remove LE config
await internalNginx.deleteLetsEncryptRequestConfig(certificate);
await internalNginx.reload();
// 6. Re-instate previously disabled hosts
await internalCertificate.enableInUseHosts(inUseResult);
} catch (err) {
// In the event of failure, revert things and throw err back
await internalNginx.deleteLetsEncryptRequestConfig(certificate);
await internalCertificate.enableInUseHosts(inUseResult);
await internalNginx.reload();
throw err;
}
}
// At this point, the letsencrypt cert should exist on disk.
// Lets get the expiry date from the file and update the row silently
try {
const certInfo = await internalCertificate.getCertificateInfoFromFile(
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
);
const savedRow = await certificateModel
.query()
.patchAndFetchById(certificate.id, {
expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
})
.then(utils.omitRow(omissions()));
// Add cert data for audit log
savedRow.meta = _.assign({}, savedRow.meta, {
letsencrypt_certificate: certInfo,
});
await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow);
return savedRow;
} catch (err) {
// Delete the certificate from the database if it was not created successfully
await certificateModel.query().deleteById(certificate.id);
throw err;
}
}
} catch (err) {
// Delete the certificate here. This is a hard delete, since it never existed properly
await certificateModel.query().deleteById(certificate.id);
throw err;
}
data.meta = _.assign({}, data.meta || {}, certificate.meta);
// Add to audit log
await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data));
return utils.omitRow(omissions())(certificate);
},
addCreatedAuditLog: async (access, certificate_id, meta) => {
await internalAuditLog.add(access, {
action: "created",
object_type: "certificate",
object_id: certificate_id,
meta: meta,
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.email]
* @param {String} [data.name]
* @return {Promise}
*/
update: async (access, data) => {
await access.can("certificates:update", data.id);
const row = await internalCertificate.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError(
`Certificate could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
const savedRow = await certificateModel
.query()
.patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()));
savedRow.meta = internalCertificate.cleanMeta(savedRow.meta);
data.meta = internalCertificate.cleanMeta(data.meta);
// Add row.nice_name for custom certs
if (savedRow.provider === "other") {
data.nice_name = savedRow.nice_name;
}
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "certificate",
object_id: row.id,
meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw
});
return savedRow;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: async (access, data) => {
const accessData = await access.can("certificates:get", data.id);
const query = certificateModel
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
const row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
return _.omit(row, [...data.omit]);
}
return internalCertificate.cleanExpansions(row);
},
cleanExpansions: (row) => {
if (typeof row.proxy_hosts !== "undefined") {
row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts);
}
if (typeof row.redirection_hosts !== "undefined") {
row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts);
}
if (typeof row.dead_hosts !== "undefined") {
row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts);
}
if (typeof row.streams !== "undefined") {
row.streams = utils.omitRows(["is_deleted"])(row.streams);
}
return row;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
download: async (access, data) => {
await access.can("certificates:get", data);
const certificate = await internalCertificate.get(access, data);
if (certificate.provider === "letsencrypt") {
const zipDirectory = internalCertificate.getLiveCertPath(data.id);
if (!fs.existsSync(zipDirectory)) {
throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`);
}
const certFiles = fs
.readdirSync(zipDirectory)
.filter((fn) => fn.endsWith(".pem"))
.map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
const downloadName = `npm-${data.id}-${Date.now()}.zip`;
const opName = `/tmp/${downloadName}`;
await internalCertificate.zipFiles(certFiles, opName);
debug(logger, "zip completed : ", opName);
return {
fileName: opName,
};
}
throw new error.ValidationError("Only Let'sEncrypt certificates can be downloaded");
},
/**
* @param {String} source
* @param {String} out
* @returns {Promise}
*/
zipFiles: async (source, out) => {
const archive = archiver("zip", { zlib: { level: 9 } });
const stream = fs.createWriteStream(out);
return new Promise((resolve, reject) => {
source.map((fl) => {
const fileName = path.basename(fl);
debug(logger, fl, "added to certificate zip");
archive.file(fl, { name: fileName });
return true;
});
archive.on("error", (err) => reject(err)).pipe(stream);
stream.on("close", () => resolve());
archive.finalize();
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("certificates:delete", data.id);
const row = await internalCertificate.get(access, { id: data.id });
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
await certificateModel.query().where("id", row.id).patch({
is_deleted: 1,
});
// Add to audit log
row.meta = internalCertificate.cleanMeta(row.meta);
await internalAuditLog.add(access, {
action: "deleted",
object_type: "certificate",
object_id: row.id,
meta: _.omit(row, omissions()),
});
if (row.provider === "letsencrypt") {
// Revoke the cert
await internalCertificate.revokeLetsEncryptSsl(row);
}
return true;
},
/**
* All Certs
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("certificates:list");
const query = certificateModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
.orderBy("nice_name", "ASC");
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string") {
query.where(function () {
this.where("nice_name", "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const r = await query.then(utils.omitRows(omissions()));
for (let i = 0; i < r.length; i++) {
r[i] = internalCertificate.cleanExpansions(r[i]);
}
return r;
},
/**
* Report use
*
* @param {Number} userId
* @param {String} visibility
* @returns {Promise}
*/
getCount: async (userId, visibility) => {
const query = certificateModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", userId);
}
const row = await query.first();
return Number.parseInt(row.count, 10);
},
/**
* @param {Object} certificate
* @returns {Promise}
*/
writeCustomCert: async (certificate) => {
logger.info("Writing Custom Certificate:", certificate);
const dir = `/data/custom_ssl/npm-${certificate.id}`;
return new Promise((resolve, reject) => {
if (certificate.provider === "letsencrypt") {
reject(new Error("Refusing to write letsencrypt certs here"));
return;
}
let certData = certificate.meta.certificate;
if (typeof certificate.meta.intermediate_certificate !== "undefined") {
certData = `${certData}\n${certificate.meta.intermediate_certificate}`;
}
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
} catch (err) {
reject(err);
return;
}
fs.writeFile(`${dir}/fullchain.pem`, certData, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}).then(() => {
return new Promise((resolve, reject) => {
fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Array} data.domain_names
* @returns {Promise}
*/
createQuickCertificate: async (access, data) => {
return await internalCertificate.create(access, {
provider: "letsencrypt",
domain_names: data.domain_names,
meta: data.meta,
});
},
/**
* Validates that the certs provided are good.
* No access required here, nothing is changed or stored.
*
* @param {Object} data
* @param {Object} data.files
* @returns {Promise}
*/
validate: (data) => {
// Put file contents into an object
const files = {};
_.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
files[name] = file.data.toString();
}
});
// For each file, create a temp file and write the contents to it
// Then test it depending on the file type
const promises = [];
_.map(files, (content, type) => {
promises.push(
new Promise((resolve) => {
if (type === "certificate_key") {
resolve(internalCertificate.checkPrivateKey(content));
} else {
// this should handle `certificate` and intermediate certificate
resolve(internalCertificate.getCertificateInfo(content, true));
}
}).then((res) => {
return { [type]: res };
}),
);
});
return Promise.all(promises).then((files) => {
let data = {};
_.each(files, (file) => {
data = _.assign({}, data, file);
});
return data;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Object} data.files
* @returns {Promise}
*/
upload: async (access, data) => {
const row = await internalCertificate.get(access, { id: data.id });
if (row.provider !== "other") {
throw new error.ValidationError("Cannot upload certificates for this type of provider");
}
const validations = await internalCertificate.validate(data);
if (typeof validations.certificate === "undefined") {
throw new error.ValidationError("Certificate file was not provided");
}
_.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
row.meta[name] = file.data.toString();
}
});
const certificate = await internalCertificate.update(access, {
id: data.id,
expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
domain_names: [validations.certificate.cn],
meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
});
certificate.meta = row.meta;
await internalCertificate.writeCustomCert(certificate);
return _.pick(row.meta, internalCertificate.allowedSslFiles);
},
/**
* Uses the openssl command to validate the private key.
* It will save the file to disk first, then run commands on it, then delete the file.
*
* @param {String} privateKey This is the entire key contents as a string
*/
checkPrivateKey: async (privateKey) => {
const filepath = await tempWrite(privateKey);
const failTimeout = setTimeout(() => {
throw new error.ValidationError(
"Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.",
);
}, 10000);
try {
const result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `);
clearTimeout(failTimeout);
if (!result.toLowerCase().includes("key is valid")) {
throw new error.ValidationError(`Result Validation Error: ${result}`);
}
fs.unlinkSync(filepath);
return true;
} catch (err) {
clearTimeout(failTimeout);
fs.unlinkSync(filepath);
throw new error.ValidationError(`Certificate Key is not valid (${err.message})`, err);
}
},
/**
* Uses the openssl command to both validate and get info out of the certificate.
* It will save the file to disk first, then run commands on it, then delete the file.
*
* @param {String} certificate This is the entire cert contents as a string
* @param {Boolean} [throwExpired] Throw when the certificate is out of date
*/
getCertificateInfo: async (certificate, throwExpired) => {
const filepath = await tempWrite(certificate);
try {
const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired);
fs.unlinkSync(filepath);
return certData;
} catch (err) {
fs.unlinkSync(filepath);
throw err;
}
},
/**
* Uses the openssl command to both validate and get info out of the certificate.
* It will save the file to disk first, then run commands on it, then delete the file.
*
* @param {String} certificateFile The file location on disk
* @param {Boolean} [throw_expired] Throw when the certificate is out of date
*/
getCertificateInfoFromFile: async (certificateFile, throw_expired) => {
const certData = {};
try {
const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]);
// Examples:
// subject=CN = *.jc21.com
// subject=CN = something.example.com
const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
const match = regex.exec(result);
if (match && typeof match[1] !== "undefined") {
certData.cn = match[1];
}
const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]);
// Examples:
// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
// issuer=C = US, O = Let's Encrypt, CN = E5
// issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA
const regex2 = /^(?:issuer=)?(.*)$/gim;
const match2 = regex2.exec(result2);
if (match2 && typeof match2[1] !== "undefined") {
certData.issuer = match2[1];
}
const result3 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-dates", "-noout"]);
// notBefore=Jul 14 04:04:29 2018 GMT
// notAfter=Oct 12 04:04:29 2018 GMT
let validFrom = null;
let validTo = null;
const lines = result3.split("\n");
lines.map((str) => {
const regex = /^(\S+)=(.*)$/gim;
const match = regex.exec(str.trim());
if (match && typeof match[2] !== "undefined") {
const date = Number.parseInt(moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), 10);
if (match[1].toLowerCase() === "notbefore") {
validFrom = date;
} else if (match[1].toLowerCase() === "notafter") {
validTo = date;
}
}
return true;
});
if (!validFrom || !validTo) {
throw new error.ValidationError(`Could not determine dates from certificate: ${result}`);
}
if (throw_expired && validTo < Number.parseInt(moment().format("X"), 10)) {
throw new error.ValidationError("Certificate has expired");
}
certData.dates = {
from: validFrom,
to: validTo,
};
return certData;
} catch (err) {
throw new error.ValidationError(`Certificate is not valid (${err.message})`, err);
}
},
/**
* Cleans the ssl keys from the meta object and sets them to "true"
*
* @param {Object} meta
* @param {Boolean} [remove]
* @returns {Object}
*/
cleanMeta: (meta, remove) => {
internalCertificate.allowedSslFiles.map((key) => {
if (typeof meta[key] !== "undefined" && meta[key]) {
if (remove) {
delete meta[key];
} else {
meta[key] = true;
}
}
return true;
});
return meta;
},
/**
* Request a certificate using the http challenge
* @param {Object} certificate the certificate row
* @param {String} email the email address to use for registration
* @returns {Promise}
*/
requestLetsEncryptSsl: async (certificate, email) => {
logger.info(
`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
const args = [
"certonly",
"--config",
letsencryptConfig,
"--work-dir",
certbotWorkDir,
"--logs-dir",
certbotLogsDir,
"--cert-name",
`npm-${certificate.id}`,
"--agree-tos",
"--authenticator",
"webroot",
"-m",
email,
"--preferred-challenges",
"http",
"--domains",
certificate.domain_names.join(","),
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.success(result);
return result;
},
/**
* @param {Object} certificate the certificate row
* @param {String} email the email address to use for registration
* @returns {Promise}
*/
requestLetsEncryptSslWithDnsChallenge: async (certificate, email) => {
await installPlugin(certificate.meta.dns_provider);
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
logger.info(
`Requesting LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true });
fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 });
// Whether the plugin has a ---credentials argument
const hasConfigArg = certificate.meta.dns_provider !== "route53";
const args = [
"certonly",
"--config",
letsencryptConfig,
"--work-dir",
certbotWorkDir,
"--logs-dir",
certbotLogsDir,
"--cert-name",
`npm-${certificate.id}`,
"--agree-tos",
"-m",
email,
"--preferred-challenges",
"dns",
"--domains",
certificate.domain_names.join(","),
"--authenticator",
dnsPlugin.full_plugin_name,
];
if (hasConfigArg) {
args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation);
}
if (certificate.meta.propagation_seconds !== undefined) {
args.push(
`--${dnsPlugin.full_plugin_name}-propagation-seconds`,
certificate.meta.propagation_seconds.toString(),
);
}
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
} catch (err) {
// Don't fail if file does not exist, so no need for action in the callback
fs.unlink(credentialsLocation, () => {});
throw err;
}
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
renew: async (access, data) => {
await access.can("certificates:update", data);
const certificate = await internalCertificate.get(access, data);
if (certificate.provider === "letsencrypt") {
const renewMethod = certificate.meta.dns_challenge
? internalCertificate.renewLetsEncryptSslWithDnsChallenge
: internalCertificate.renewLetsEncryptSsl;
await renewMethod(certificate);
const certInfo = await internalCertificate.getCertificateInfoFromFile(
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
);
const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, {
expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
});
// Add to audit log
await internalAuditLog.add(access, {
action: "renewed",
object_type: "certificate",
object_id: updatedCertificate.id,
meta: updatedCertificate,
});
return updatedCertificate;
}
throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");
},
/**
* @param {Object} certificate the certificate row
* @returns {Promise}
*/
renewLetsEncryptSsl: async (certificate) => {
logger.info(
`Renewing LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
const args = [
"renew",
"--force-renewal",
"--config",
letsencryptConfig,
"--work-dir",
certbotWorkDir,
"--logs-dir",
certbotLogsDir,
"--cert-name",
`npm-${certificate.id}`,
"--preferred-challenges",
"http",
"--no-random-sleep-on-renew",
"--disable-hook-validation",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
},
/**
* @param {Object} certificate the certificate row
* @returns {Promise}
*/
renewLetsEncryptSslWithDnsChallenge: async (certificate) => {
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
if (!dnsPlugin) {
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
}
logger.info(
`Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
const args = [
"renew",
"--force-renewal",
"--config",
letsencryptConfig,
"--work-dir",
certbotWorkDir,
"--logs-dir",
certbotLogsDir,
"--cert-name",
`npm-${certificate.id}`,
"--preferred-challenges",
"dns",
"--disable-hook-validation",
"--no-random-sleep-on-renew",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
},
/**
* @param {Object} certificate the certificate row
* @param {Boolean} [throwErrors]
* @returns {Promise}
*/
revokeLetsEncryptSsl: async (certificate, throwErrors) => {
logger.info(
`Revoking LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
const args = [
"revoke",
"--config",
letsencryptConfig,
"--work-dir",
certbotWorkDir,
"--logs-dir",
certbotLogsDir,
"--cert-path",
`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`,
"--delete-after-revoke",
];
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`);
logger.info(result);
return result;
} catch (err) {
logger.error(err.message);
if (throwErrors) {
throw err;
}
}
},
/**
* @param {Object} certificate
* @returns {Boolean}
*/
hasLetsEncryptSslCerts: (certificate) => {
const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id);
return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`);
},
/**
* @param {Object} inUseResult
* @param {Number} inUseResult.total_count
* @param {Array} inUseResult.proxy_hosts
* @param {Array} inUseResult.redirection_hosts
* @param {Array} inUseResult.dead_hosts
* @returns {Promise}
*/
disableInUseHosts: async (inUseResult) => {
if (inUseResult?.total_count) {
if (inUseResult?.proxy_hosts.length) {
await internalNginx.bulkDeleteConfigs("proxy_host", inUseResult.proxy_hosts);
}
if (inUseResult?.redirection_hosts.length) {
await internalNginx.bulkDeleteConfigs("redirection_host", inUseResult.redirection_hosts);
}
if (inUseResult?.dead_hosts.length) {
await internalNginx.bulkDeleteConfigs("dead_host", inUseResult.dead_hosts);
}
}
},
/**
* @param {Object} inUseResult
* @param {Number} inUseResult.total_count
* @param {Array} inUseResult.proxy_hosts
* @param {Array} inUseResult.redirection_hosts
* @param {Array} inUseResult.dead_hosts
* @returns {Promise}
*/
enableInUseHosts: async (inUseResult) => {
if (inUseResult.total_count) {
if (inUseResult.proxy_hosts.length) {
await internalNginx.bulkGenerateConfigs("proxy_host", inUseResult.proxy_hosts);
}
if (inUseResult.redirection_hosts.length) {
await internalNginx.bulkGenerateConfigs("redirection_host", inUseResult.redirection_hosts);
}
if (inUseResult.dead_hosts.length) {
await internalNginx.bulkGenerateConfigs("dead_host", inUseResult.dead_hosts);
}
}
},
/**
*
* @param {Object} payload
* @param {string[]} payload.domains
* @returns
*/
testHttpsChallenge: async (access, payload) => {
await access.can("certificates:list");
// Create a test challenge file
const testChallengeDir = "/data/letsencrypt-acme-challenge/.well-known/acme-challenge";
const testChallengeFile = `${testChallengeDir}/test-challenge`;
fs.mkdirSync(testChallengeDir, { recursive: true });
fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" });
const results = {};
for (const domain of payload.domains) {
results[domain] = await internalCertificate.performTestForDomain(domain);
}
// Remove the test challenge file
fs.unlinkSync(testChallengeFile);
return results;
},
performTestForDomain: async (domain) => {
logger.info(`Testing http challenge for ${domain}`);
const agent = new ProxyAgent();
const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
const options = {
method: "POST",
headers: {
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(formBody),
},
agent,
};
const result = await new Promise((resolve) => {
const req = https.request("https://www.site24x7.com/tools/restapi-tester", options, (res) => {
let responseBody = "";
res.on("data", (chunk) => {
responseBody = responseBody + chunk;
});
res.on("end", () => {
try {
const parsedBody = JSON.parse(`${responseBody}`);
if (res.statusCode !== 200) {
logger.warn(
`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`,
);
resolve(undefined);
} else {
resolve(parsedBody);
}
} catch (err) {
if (res.statusCode !== 200) {
logger.warn(
`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`,
);
} else {
logger.warn(
`Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`,
);
}
resolve(undefined);
}
});
});
// Make sure to write the request body.
req.write(formBody);
req.end();
req.on("error", (e) => {
logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
resolve(undefined);
});
});
if (!result) {
// Some error occurred while trying to get the data
return "failed";
}
if (result.error) {
logger.info(
`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`,
);
return `other:${result.error.msg}`;
}
if (`${result.responsecode}` === "200" && result.htmlresponse === "Success") {
// Server exists and has responded with the correct data
return "ok";
}
if (`${result.responsecode}` === "200") {
// Server exists but has responded with wrong data
logger.info(
`HTTP challenge test failed for domain ${domain} because of invalid returned data:`,
result.htmlresponse,
);
return "wrong-data";
}
if (`${result.responsecode}` === "404") {
// Server exists but responded with a 404
logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`);
return "404";
}
if (
`${result.responsecode}` === "0" ||
(typeof result.reason === "string" && result.reason.toLowerCase() === "host unavailable")
) {
// Server does not exist at domain
logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`);
return "no-host";
}
// Other errors
logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`);
return `other:${result.responsecode}`;
},
getAdditionalCertbotArgs: (certificate_id, dns_provider) => {
const args = [];
if (useLetsencryptServer() !== null) {
args.push("--server", useLetsencryptServer());
}
if (useLetsencryptStaging() && useLetsencryptServer() === null) {
args.push("--staging");
}
// For route53, add the credentials file as an environment variable,
// inheriting the process env
const opts = {};
if (certificate_id && dns_provider === "route53") {
opts.env = process.env;
opts.env.AWS_CONFIG_FILE = `/etc/letsencrypt/credentials/credentials-${certificate_id}`;
}
if (dns_provider === "duckdns") {
args.push("--dns-duckdns-no-txt-restore");
}
return { args: args, opts: opts };
},
getLiveCertPath: (certificateId) => {
return `/etc/letsencrypt/live/npm-${certificateId}`;
},
};
export default internalCertificate;
================================================
FILE: backend/internal/dead-host.js
================================================
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import deadHostModel from "../models/dead_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const omissions = () => {
return ["is_deleted"];
};
const internalDeadHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
const createCertificate = data.certificate_id === "new";
if (createCertificate) {
delete data.certificate_id;
}
await access.can("dead_hosts:create", data);
// Get a list of the domain names and check each of them against existing records
const domainNameCheckPromises = [];
data.domain_names.map((domain_name) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
await Promise.all(domainNameCheckPromises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
const thisData = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
thisData.advanced_config = "";
}
const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, data);
// update host with cert id
await internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
}
// re-fetch with cert
const freshRow = await internalDeadHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
// Sanity check
if (createCertificate && !freshRow.certificate_id) {
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
}
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
return freshRow;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @return {Promise}
*/
update: async (access, data) => {
const createCertificate = data.certificate_id === "new";
if (createCertificate) {
delete data.certificate_id;
}
await access.can("dead_hosts:update", data.id);
// Get a list of the domain names and check each of them against existing records
const domainNameCheckPromises = [];
if (typeof data.domain_names !== "undefined") {
data.domain_names.map((domainName) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id));
return true;
});
const checkResults = await Promise.all(domainNameCheckPromises);
checkResults.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
}
const row = await internalDeadHost.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta),
});
// update host with cert id
data.certificate_id = cert.id;
}
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
let thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update
await deadHostModel
.query()
.where({id: data.id})
.patch(data);
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
const thisRow = await internalDeadHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
});
// Configure nginx
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
row.meta = newMeta;
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: async (access, data) => {
const accessData = await access.can("dead_hosts:get", data.id);
const query = deadHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph(deadHostModel.defaultAllowGraph)
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
const row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
return _.omit(row, data.omit);
}
return row;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("dead_hosts:delete", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
await deadHostModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
});
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row);
// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "disabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
* All Hosts
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("dead_hosts:list")
const query = deadHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(deadHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
},
/**
* Report use
*
* @param {Number} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: async (user_id, visibility) => {
const query = deadHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
const row = await query.first();
return Number.parseInt(row.count, 10);
},
};
export default internalDeadHost;
================================================
FILE: backend/internal/host.js
================================================
import _ from "lodash";
import { castJsonIfNeed } from "../lib/helpers.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
const internalHost = {
/**
* Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off.
* if force_ssl is off, then hsts_enabled is definitely off.
*
* @param {object} data
* @param {object} [existing_data]
* @returns {object}
*/
cleanSslHstsData: (data, existingData) => {
const combinedData = _.assign({}, existingData || {}, data);
if (!combinedData.certificate_id) {
combinedData.ssl_forced = false;
combinedData.http2_support = false;
}
if (!combinedData.ssl_forced) {
combinedData.hsts_enabled = false;
}
if (!combinedData.hsts_enabled) {
combinedData.hsts_subdomains = false;
}
return combinedData;
},
/**
* used by the getAll functions of hosts, this removes the certificate meta if present
*
* @param {Array} rows
* @returns {Array}
*/
cleanAllRowsCertificateMeta: (rows) => {
rows.map((_, idx) => {
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) {
rows[idx].certificate.meta = {};
}
return true;
});
return rows;
},
/**
* used by the get/update functions of hosts, this removes the certificate meta if present
*
* @param {Object} row
* @returns {Object}
*/
cleanRowCertificateMeta: (row) => {
if (typeof row.certificate !== "undefined" && row.certificate) {
row.certificate.meta = {};
}
return row;
},
/**
* This returns all the host types with any domain listed in the provided domainNames array.
* This is used by the certificates to temporarily disable any host that is using the domain
*
* @param {Array} domainNames
* @returns {Promise}
*/
getHostsWithDomains: async (domainNames) => {
const responseObject = {
total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: [],
};
const proxyRes = await proxyHostModel.query().where("is_deleted", 0);
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
responseObject.total_count += responseObject.proxy_hosts.length;
const redirRes = await redirectionHostModel.query().where("is_deleted", 0);
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);
responseObject.total_count += responseObject.redirection_hosts.length;
const deadRes = await deadHostModel.query().where("is_deleted", 0);
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);
responseObject.total_count += responseObject.dead_hosts.length;
return responseObject;
},
/**
* Internal use only, checks to see if the domain is already taken by any other record
*
* @param {String} hostname
* @param {String} [ignore_type] 'proxy', 'redirection', 'dead'
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
* @returns {Promise}
*/
isHostnameTaken: (hostname, ignore_type, ignore_id) => {
const promises = [
proxyHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
redirectionHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
deadHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
];
return Promise.all(promises).then((promises_results) => {
let is_taken = false;
if (promises_results[0]) {
// Proxy Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[0],
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
if (promises_results[1]) {
// Redirection Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[1],
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
if (promises_results[2]) {
// Dead Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[2],
ignore_type === "dead" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
return {
hostname: hostname,
is_taken: is_taken,
};
});
},
/**
* Private call only
*
* @param {String} hostname
* @param {Array} existingRows
* @param {Integer} [ignoreId]
* @returns {Boolean}
*/
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {
let isTaken = false;
if (existingRows?.length) {
existingRows.map((existingRow) => {
existingRow.domain_names.map((existingHostname) => {
// Does this domain match?
if (existingHostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignoreId || ignoreId !== existingRow.id) {
isTaken = true;
}
}
return true;
});
return true;
});
}
return isTaken;
},
/**
* Private call only
*
* @param {Array} hosts
* @param {Array} domainNames
* @returns {Array}
*/
_getHostsWithDomains: (hosts, domainNames) => {
const response = [];
if (hosts?.length) {
hosts.map((host) => {
let hostMatches = false;
domainNames.map((domainName) => {
host.domain_names.map((hostDomainName) => {
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) {
hostMatches = true;
}
return true;
});
return true;
});
if (hostMatches) {
response.push(host);
}
return true;
});
}
return response;
},
};
export default internalHost;
================================================
FILE: backend/internal/ip_ranges.js
================================================
import fs from "node:fs";
import https from "node:https";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { ProxyAgent } from "proxy-agent";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { ipRanges as logger } from "../logger.js";
import internalNginx from "./nginx.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const CLOUDFRONT_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
const CLOUDFARE_V4_URL = "https://www.cloudflare.com/ips-v4";
const CLOUDFARE_V6_URL = "https://www.cloudflare.com/ips-v6";
const regIpV4 = /^(\d+\.?){4}\/\d+/;
const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/;
const internalIpRanges = {
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_processing: false,
iteration_count: 0,
initTimer: () => {
logger.info("IP Ranges Renewal Timer initialized");
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
},
fetchUrl: (url) => {
const agent = new ProxyAgent();
return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`);
return https
.get(url, { agent }, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {
raw_data += chunk;
});
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
});
});
},
/**
* Triggered at startup and then later by a timer, this will fetch the ip ranges from services and apply them to nginx.
*/
fetch: () => {
if (!internalIpRanges.interval_processing) {
internalIpRanges.interval_processing = true;
logger.info("Fetching IP Ranges from online services...");
let ip_ranges = [];
return internalIpRanges
.fetchUrl(CLOUDFRONT_URL)
.then((cloudfront_data) => {
const data = JSON.parse(cloudfront_data);
if (data && typeof data.prefixes !== "undefined") {
data.prefixes.map((item) => {
if (item.service === "CLOUDFRONT") {
ip_ranges.push(item.ip_prefix);
}
return true;
});
}
if (data && typeof data.ipv6_prefixes !== "undefined") {
data.ipv6_prefixes.map((item) => {
if (item.service === "CLOUDFRONT") {
ip_ranges.push(item.ipv6_prefix);
}
return true;
});
}
})
.then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
})
.then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line));
ip_ranges = [...ip_ranges, ...items];
})
.then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
})
.then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line));
ip_ranges = [...ip_ranges, ...items];
})
.then(() => {
const clean_ip_ranges = [];
ip_ranges.map((range) => {
if (range) {
clean_ip_ranges.push(range);
}
return true;
});
return internalIpRanges.generateConfig(clean_ip_ranges).then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
});
})
.then(() => {
internalIpRanges.interval_processing = false;
internalIpRanges.iteration_count++;
})
.catch((err) => {
logger.fatal(err.message);
internalIpRanges.interval_processing = false;
});
}
},
/**
* @param {Array} ip_ranges
* @returns {Promise}
*/
generateConfig: (ip_ranges) => {
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
const filename = "/etc/nginx/conf.d/include/ip_ranges.conf";
try {
template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" });
} catch (err) {
reject(new errs.ConfigurationError(err.message));
return;
}
renderEngine
.parseAndRender(template, { ip_ranges: ip_ranges })
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
resolve(true);
})
.catch((err) => {
logger.warn(`Could not write ${filename}: ${err.message}`);
reject(new errs.ConfigurationError(err.message));
});
});
},
};
export default internalIpRanges;
================================================
FILE: backend/internal/nginx.js
================================================
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { debug, nginx as logger } from "../logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const internalNginx = {
/**
* This will:
* - test the nginx config first to make sure it's OK
* - create / recreate the config for the host
* - test again
* - IF OK: update the meta with online status
* - IF BAD: update the meta with offline status and remove the config entirely
* - then reload nginx
*
* @param {Object|String} model
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
configure: (model, host_type, host) => {
let combined_meta = {};
return internalNginx
.test()
.then(() => {
// Nginx is OK
// We're deleting this config regardless.
// Don't throw errors, as the file may not exist at all
// Delete the .err file too
return internalNginx.deleteConfig(host_type, host, false, true);
})
.then(() => {
return internalNginx.generateConfig(host_type, host);
})
.then(() => {
// Test nginx again and update meta with result
return internalNginx
.test()
.then(() => {
// nginx is ok
combined_meta = _.assign({}, host.meta, {
nginx_online: true,
nginx_err: null,
});
return model.query().where("id", host.id).patch({
meta: combined_meta,
});
})
.catch((err) => {
// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
// It will always look like this:
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
const valid_lines = [];
const err_lines = err.message.split("\n");
err_lines.map((line) => {
if (line.indexOf("/var/log/nginx/error.log") === -1) {
valid_lines.push(line);
}
return true;
});
debug(logger, "Nginx test failed:", valid_lines.join("\n"));
// config is bad, update meta and delete config
combined_meta = _.assign({}, host.meta, {
nginx_online: false,
nginx_err: valid_lines.join("\n"),
});
return model
.query()
.where("id", host.id)
.patch({
meta: combined_meta,
})
.then(() => {
internalNginx.renameConfigAsError(host_type, host);
})
.then(() => {
return internalNginx.deleteConfig(host_type, host, true);
});
});
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
return combined_meta;
});
},
/**
* @returns {Promise}
*/
test: () => {
debug(logger, "Testing Nginx configuration");
return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]);
},
/**
* @returns {Promise}
*/
reload: () => {
return internalNginx.test().then(() => {
logger.info("Reloading Nginx");
return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]);
});
},
/**
* @param {String} host_type
* @param {Integer} host_id
* @returns {String}
*/
getConfigName: (host_type, host_id) => {
if (host_type === "default") {
return "/data/nginx/default_host/site.conf";
}
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`;
},
/**
* Generates custom locations
* @param {Object} host
* @returns {Promise}
*/
renderLocations: (host) => {
return new Promise((resolve, reject) => {
let template;
try {
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" });
} catch (err) {
reject(new errs.ConfigurationError(err.message));
return;
}
const renderEngine = utils.getRenderEngine();
let renderedLocations = "";
const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) {
const locationCopy = Object.assign(
{},
{ access_list_id: host.access_list_id },
{ certificate_id: host.certificate_id },
{ ssl_forced: host.ssl_forced },
{ caching_enabled: host.caching_enabled },
{ block_exploits: host.block_exploits },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);
if (locationCopy.forward_host.indexOf("/") > -1) {
const splitted = locationCopy.forward_host.split("/");
locationCopy.forward_host = splitted.shift();
locationCopy.forward_path = `/${splitted.join("/")}`;
}
renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
}
};
locationRendering().then(() => resolve(renderedLocations));
});
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
generateConfig: (host_type, host_row) => {
// Prevent modifying the original object:
const host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2));
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
const filename = internalNginx.getConfigName(nice_host_type, host.id);
try {
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" });
} catch (err) {
reject(new errs.ConfigurationError(err.message));
return;
}
let locationsPromise;
let origLocations;
// Manipulate the data a bit before sending it to the template
if (nice_host_type !== "default") {
host.use_default_location = true;
if (typeof host.advanced_config !== "undefined" && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
// For redirection hosts, if the scheme is not http or https, set it to $scheme
if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {
host.forward_scheme = "$scheme";
}
if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations);
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
host.locations = renderedLocations;
});
// Allow someone who is using / custom location path to use it, and skip the default / location
_.map(host.locations, (location) => {
if (location.path === "/") {
host.use_default_location = false;
}
});
} else {
locationsPromise = Promise.resolve();
}
// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();
locationsPromise.then(() => {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
// Restore locations array
host.locations = origLocations;
resolve(true);
})
.catch((err) => {
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
});
},
/**
* This generates a temporary nginx config listening on port 80 for the domain names listed
* in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt
* when requesting a certificate without having a hostname set up already.
*
* @param {Object} certificate
* @returns {Promise}
*/
generateLetsEncryptRequestConfig: (certificate) => {
debug(logger, "Generating LetsEncrypt Request Config:", certificate);
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
try {
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" });
} catch (err) {
reject(new errs.ConfigurationError(err.message));
return;
}
certificate.ipv6 = internalNginx.ipv6Enabled();
renderEngine
.parseAndRender(template, certificate)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
resolve(true);
})
.catch((err) => {
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
},
/**
* A simple wrapper around unlinkSync that writes to the logger
*
* @param {String} filename
*/
deleteFile: (filename) => {
if (!fs.existsSync(filename)) {
return;
}
try {
debug(logger, `Deleting file: ${filename}`);
fs.unlinkSync(filename);
} catch (err) {
debug(logger, "Could not delete file:", JSON.stringify(err, null, 2));
}
},
/**
*
* @param {String} host_type
* @returns String
*/
getFileFriendlyHostType: (host_type) => {
return host_type.replace(/-/g, "_");
},
/**
* This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`
*
* @param {Object} certificate
* @returns {Promise}
*/
deleteLetsEncryptRequestConfig: (certificate) => {
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
resolve();
});
},
/**
* @param {String} host_type
* @param {Object} [host]
* @param {Boolean} [delete_err_file]
* @returns {Promise}
*/
deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
if (delete_err_file) {
internalNginx.deleteFile(config_file_err);
}
resolve();
});
},
/**
* @param {String} host_type
* @param {Object} [host]
* @returns {Promise}
*/
renameConfigAsError: (host_type, host) => {
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve /*, reject*/) => {
fs.unlink(config_file, () => {
// ignore result, continue
fs.rename(config_file, config_file_err, () => {
// also ignore result, as this is a debugging informative file anyway
resolve();
});
});
});
},
/**
* @param {String} hostType
* @param {Array} hosts
* @returns {Promise}
*/
bulkGenerateConfigs: (hostType, hosts) => {
const promises = [];
hosts.map((host) => {
promises.push(internalNginx.generateConfig(hostType, host));
return true;
});
return Promise.all(promises);
},
/**
* @param {String} host_type
* @param {Array} hosts
* @returns {Promise}
*/
bulkDeleteConfigs: (host_type, hosts) => {
const promises = [];
hosts.map((host) => {
promises.push(internalNginx.deleteConfig(host_type, host, true));
return true;
});
return Promise.all(promises);
},
/**
* @param {string} config
* @returns {boolean}
*/
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im),
/**
* @returns {boolean}
*/
ipv6Enabled: () => {
if (typeof process.env.DISABLE_IPV6 !== "undefined") {
const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes");
}
return true;
},
};
export default internalNginx;
================================================
FILE: backend/internal/proxy-host.js
================================================
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import proxyHostModel from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const omissions = () => {
return ["is_deleted", "owner.is_deleted"];
};
const internalProxyHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let thisData = data;
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) {
delete thisData.certificate_id;
}
return access
.can("proxy_hosts:create", thisData)
.then(() => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
})
.then(() => {
// At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
// Fix for db field not having a default value
// for this optional field.
if (typeof thisData.advanced_config === "undefined") {
thisData.advanced_config = "";
}
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
.then((cert) => {
// update host with cert id
return internalProxyHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalProxyHost.get(access, {
id: row.id,
expand: ["certificate", "owner", "access_list.[clients,items]"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
return row;
});
})
.then((row) => {
// Audit log
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @return {Promise}
*/
update: (access, data) => {
let thisData = data;
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) {
delete thisData.certificate_id;
}
return access
.can("proxy_hosts:update", thisData.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
return domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
);
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
}
})
.then(() => {
return internalProxyHost.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
return proxyHostModel
.query()
.where({ id: thisData.id })
.patch(thisData)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return saved_row;
});
});
})
.then(() => {
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
.then((row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
}
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("proxy_hosts:get", thisData.id)
.then((access_data) => {
const query = proxyHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(proxyHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
const thisRow = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
return thisRow;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("proxy_hosts:delete", data.id)
.then(() => {
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
return proxyHostModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("proxy_hosts:update", data.id)
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ["certificate", "owner", "access_list"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
return proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("proxy_hosts:update", data.id)
.then(() => {
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
return proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* All Hosts
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("proxy_hosts:list");
const query = proxyHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(proxyHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
},
/**
* Report use
*
* @param {Number} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
const query = proxyHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
export default internalProxyHost;
================================================
FILE: backend/internal/redirection-host.js
================================================
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import redirectionHostModel from "../models/redirection_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const omissions = () => {
return ["is_deleted"];
};
const internalRedirectionHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) {
delete thisData.certificate_id;
}
return access
.can("redirection_hosts:create", thisData)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
})
.then(() => {
// At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
data.advanced_config = "";
}
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
.then((cert) => {
// update host with cert id
return internalRedirectionHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalRedirectionHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => {
return row;
});
})
.then((row) => {
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @return {Promise}
*/
update: (access, data) => {
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) {
delete thisData.certificate_id;
}
return access
.can("redirection_hosts:update", thisData.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "redirection", thisData.id),
);
return true;
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
}
})
.then(() => {
return internalRedirectionHost.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
thisData,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
return redirectionHostModel
.query()
.where({ id: thisData.id })
.patch(thisData)
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalRedirectionHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
})
.then((row) => {
// Configure nginx
return internalNginx
.configure(redirectionHostModel, "redirection_host", row)
.then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("redirection_hosts:get", thisData.id)
.then((access_data) => {
const query = redirectionHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(redirectionHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(thisRow, thisData.omit);
}
return thisRow;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("redirection_hosts:delete", data.id)
.then(() => {
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
return redirectionHostModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("redirection_hosts:update", data.id)
.then(() => {
return internalRedirectionHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
return redirectionHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("redirection_hosts:update", data.id)
.then(() => {
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
return redirectionHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* All Hosts
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access
.can("redirection_hosts:list")
.then((access_data) => {
const query = redirectionHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(redirectionHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},
/**
* Report use
*
* @param {Number} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
export default internalRedirectionHost;
================================================
FILE: backend/internal/remote-version.js
================================================
import https from "node:https";
import { ProxyAgent } from "proxy-agent";
import { debug, remoteVersion as logger } from "../logger.js";
import pjson from "../package.json" with { type: "json" };
const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
const internalRemoteVersion = {
cache_timeout: 1000 * 60 * 15, // 15 minutes
last_result: null,
last_fetch_time: null,
/**
* Fetch the latest version info, using a cached result if within the cache timeout period.
* @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info
*/
get: async () => {
if (
!internalRemoteVersion.last_result ||
!internalRemoteVersion.last_fetch_time ||
Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout
) {
const raw = await internalRemoteVersion.fetchUrl(VERSION_URL);
const data = JSON.parse(raw);
internalRemoteVersion.last_result = data;
internalRemoteVersion.last_fetch_time = Date.now();
} else {
debug(logger, "Using cached remote version result");
}
const latestVersion = internalRemoteVersion.last_result.tag_name;
const version = pjson.version.split("-").shift().split(".");
const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
return {
current: currentVersion,
latest: latestVersion,
update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),
};
},
fetchUrl: (url) => {
const agent = new ProxyAgent();
const headers = {
"User-Agent": `NginxProxyManager v${pjson.version}`,
};
return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`);
return https
.get(url, { agent, headers }, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {
raw_data += chunk;
});
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
});
});
},
compareVersions: (current, latest) => {
const cleanCurrent = current.replace(/^v/, "");
const cleanLatest = latest.replace(/^v/, "");
const currentParts = cleanCurrent.split(".").map(Number);
const latestParts = cleanLatest.split(".").map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const curr = currentParts[i] || 0;
const lat = latestParts[i] || 0;
if (lat > curr) return true;
if (lat < curr) return false;
}
return false;
},
};
export default internalRemoteVersion;
================================================
FILE: backend/internal/report.js
================================================
import internalDeadHost from "./dead-host.js";
import internalProxyHost from "./proxy-host.js";
import internalRedirectionHost from "./redirection-host.js";
import internalStream from "./stream.js";
const internalReport = {
/**
* @param {Access} access
* @return {Promise}
*/
getHostsReport: (access) => {
return access
.can("reports:hosts", 1)
.then((access_data) => {
const userId = access.token.getUserId(1);
const promises = [
internalProxyHost.getCount(userId, access_data.permission_visibility),
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
internalStream.getCount(userId, access_data.permission_visibility),
internalDeadHost.getCount(userId, access_data.permission_visibility),
];
return Promise.all(promises);
})
.then((counts) => {
return {
proxy: counts.shift(),
redirection: counts.shift(),
stream: counts.shift(),
dead: counts.shift(),
};
});
},
};
export default internalReport;
================================================
FILE: backend/internal/setting.js
================================================
import fs from "node:fs";
import errs from "../lib/error.js";
import settingModel from "../models/setting.js";
import internalNginx from "./nginx.js";
const internalSetting = {
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
update: (access, data) => {
return access
.can("settings:update", data.id)
.then((/*access_data*/) => {
return internalSetting.get(access, { id: data.id });
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
return settingModel.query().where({ id: data.id }).patch(data);
})
.then(() => {
return internalSetting.get(access, {
id: data.id,
});
})
.then((row) => {
if (row.id === "default-site") {
// write the html if we need to
if (row.value === "html") {
fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" });
}
// Configure nginx
return internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.generateConfig("default", row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
return row;
})
.catch((/*err*/) => {
internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
// I'm being slack here I know..
throw new errs.ValidationError("Could not reconfigure Nginx. Please check logs.");
});
});
}
return row;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
get: (access, data) => {
return access
.can("settings:get", data.id)
.then(() => {
return settingModel.query().where("id", data.id).first();
})
.then((row) => {
if (row) {
return row;
}
throw new errs.ItemNotFoundError(data.id);
});
},
/**
* This will only count the settings
*
* @param {Access} access
* @returns {*}
*/
getCount: (access) => {
return access
.can("settings:list")
.then(() => {
return settingModel.query().count("id as count").first();
})
.then((row) => {
return Number.parseInt(row.count, 10);
});
},
/**
* All settings
*
* @param {Access} access
* @returns {Promise}
*/
getAll: (access) => {
return access.can("settings:list").then(() => {
return settingModel.query().orderBy("description", "ASC");
});
},
};
export default internalSetting;
================================================
FILE: backend/internal/stream.js
================================================
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import streamModel from "../models/stream.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"];
};
const internalStream = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
const create_certificate = data.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("streams:create", data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
data.owner_user_id = access.token.getUserId(1);
if (typeof data.meta === "undefined") {
data.meta = {};
}
// streams aren't routed by domain name so don't store domain names in the DB
const data_no_domains = structuredClone(data);
delete data_no_domains.domain_names;
return streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, "stream", row).then(() => {
return row;
});
})
.then((row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "stream",
object_id: row.id,
meta: data,
})
.then(() => {
return row;
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @return {Promise}
*/
update: (access, data) => {
let thisData = data;
const create_certificate = thisData.certificate_id === "new";
if (create_certificate) {
delete thisData.certificate_id;
}
return access
.can("streams:update", thisData.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
return internalStream.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
thisData,
);
return streamModel
.query()
.patchAndFetchById(row.id, thisData)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "stream",
object_id: row.id,
meta: thisData,
})
.then(() => {
return saved_row;
});
});
})
.then(() => {
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => {
return internalNginx.configure(streamModel, "stream", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("streams:get", thisData.id)
.then((access_data) => {
const query = streamModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(streamModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(thisRow, thisData.omit);
}
return thisRow;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("streams:delete", data.id)
.then(() => {
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
return streamModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("streams:update", data.id)
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Stream is already enabled");
}
row.enabled = 1;
return streamModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(streamModel, "stream", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("streams:update", data.id)
.then(() => {
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Stream is already disabled");
}
row.enabled = 0;
return streamModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
/**
* All Streams
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access
.can("streams:list")
.then((access_data) => {
const query = streamModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(streamModel.defaultAllowGraph)
.orderBy("incoming_port", "ASC");
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},
/**
* Report use
*
* @param {Number} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
const query = streamModel.query().count("id AS count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
export default internalStream;
================================================
FILE: backend/internal/token.js
================================================
import _ from "lodash";
import errs from "../lib/error.js";
import { parseDatePeriod } from "../lib/helpers.js";
import authModel from "../models/auth.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import twoFactor from "./2fa.js";
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
export default {
/**
* @param {Object} data
* @param {String} data.identity
* @param {String} data.secret
* @param {String} [data.scope]
* @param {String} [data.expiry]
* @param {String} [issuer]
* @returns {Promise}
*/
getTokenFromEmail: async (data, issuer) => {
const Token = TokenModel();
data.scope = data.scope || "user";
data.expiry = data.expiry || "1d";
const user = await userModel
.query()
.where("email", data.identity.toLowerCase().trim())
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.first();
if (!user) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
if (!auth) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
const valid = await auth.verifyPassword(data.secret);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_AUTH,
ERROR_MESSAGE_INVALID_AUTH_I18N,
);
}
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
}
// Check if 2FA is enabled
const has2FA = await twoFactor.isEnabled(user.id);
if (has2FA) {
// Return challenge token instead of full token
const challengeToken = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: ["2fa-challenge"],
expiresIn: "5m",
});
return {
requires_2fa: true,
challenge_token: challengeToken.token,
};
}
// Create a moment of the expiry expression
const expiry = parseDatePeriod(data.expiry);
if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
}
const signed = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: [data.scope],
expiresIn: data.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
},
/**
* @param {Access} access
* @param {Object} [data]
* @param {String} [data.expiry]
* @param {String} [data.scope] Only considered if existing token scope is admin
* @returns {Promise}
*/
getFreshToken: async (access, data) => {
const Token = TokenModel();
const thisData = data || {};
thisData.expiry = thisData.expiry || "1d";
if (access?.token.getUserId(0)) {
// Create a moment of the expiry expression
const expiry = parseDatePeriod(thisData.expiry);
if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);
}
const token_attrs = {
id: access.token.getUserId(0),
};
// Only admins can request otherwise scoped tokens
let scope = access.token.get("scope");
if (thisData.scope && access.token.hasScope("admin")) {
scope = [thisData.scope];
if (thisData.scope === "job-board" || thisData.scope === "worker") {
token_attrs.id = 0;
}
}
const signed = await Token.create({
iss: "api",
scope: scope,
attrs: token_attrs,
expiresIn: thisData.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
}
throw new error.AssertionFailedError("Existing token contained invalid user data");
},
/**
* Verify 2FA code and return full token
* @param {string} challengeToken
* @param {string} code
* @param {string} [expiry]
* @returns {Promise}
*/
verify2FA: async (challengeToken, code, expiry) => {
const Token = TokenModel();
const tokenExpiry = expiry || "1d";
// Verify challenge token
let tokenData;
try {
tokenData = await Token.load(challengeToken);
} catch {
throw new errs.AuthError("Invalid or expired challenge token");
}
// Check scope
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
throw new errs.AuthError("Invalid challenge token");
}
const userId = tokenData.attrs?.id;
if (!userId) {
throw new errs.AuthError("Invalid challenge token");
}
// Verify 2FA code
const valid = await twoFactor.verifyForLogin(userId, code);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_2FA,
ERROR_MESSAGE_INVALID_2FA_I18N,
);
}
// Create full token
const expiryDate = parseDatePeriod(tokenExpiry);
if (expiryDate === null) {
throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
}
const signed = await Token.create({
iss: "api",
attrs: {
id: userId,
},
scope: ["user"],
expiresIn: tokenExpiry,
});
return {
token: signed.token,
expires: expiryDate.toISOString(),
};
},
/**
* @param {Object} user
* @returns {Promise}
*/
getTokenFromUser: async (user) => {
const expire = "1d";
const Token = TokenModel();
const expiry = parseDatePeriod(expire);
const signed = await Token.create({
iss: "api",
attrs: {
id: user.id,
},
scope: ["user"],
expiresIn: expire,
});
return {
token: signed.token,
expires: expiry.toISOString(),
user: user,
};
},
};
================================================
FILE: backend/internal/user.js
================================================
import gravatar from "gravatar";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import authModel from "../models/auth.js";
import userModel from "../models/user.js";
import userPermissionModel from "../models/user_permission.js";
import internalAuditLog from "./audit-log.js";
import internalToken from "./token.js";
const omissions = () => {
return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"];
};
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" });
const internalUser = {
/**
* Create a user can happen unauthenticated only once and only when no active users exist.
* Otherwise, a valid auth method is required.
*
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
const auth = data.auth || null;
delete data.auth;
data.avatar = data.avatar || "";
data.roles = data.roles || [];
if (typeof data.is_disabled !== "undefined") {
data.is_disabled = data.is_disabled ? 1 : 0;
}
await access.can("users:create", data);
data.avatar = gravatar.url(data.email, { default: "mm" });
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
if (auth) {
user = await authModel.query().insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {},
});
}
// Create permissions row as well
const isAdmin = data.roles.indexOf("admin") !== -1;
await userPermissionModel.query().insert({
user_id: user.id,
visibility: isAdmin ? "all" : "user",
proxy_hosts: "manage",
redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
});
user = await internalUser.get(access, { id: user.id, expand: ["permissions"] });
await internalAuditLog.add(access, {
action: "created",
object_type: "user",
object_id: user.id,
meta: user,
});
return user;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.email]
* @param {String} [data.name]
* @return {Promise}
*/
update: (access, data) => {
if (typeof data.is_disabled !== "undefined") {
data.is_disabled = data.is_disabled ? 1 : 0;
}
return access
.can("users:update", data.id)
.then(() => {
// Make sure that the user being updated doesn't change their email to another user that is already using it
// 1. get user we want to update
return internalUser.get(access, { id: data.id }).then((user) => {
// 2. if email is to be changed, find other users with that email
if (typeof data.email !== "undefined") {
data.email = data.email.toLowerCase().trim();
if (user.email !== data.email) {
return internalUser.isEmailAvailable(data.email, data.id).then((available) => {
if (!available) {
throw new errs.ValidationError(`Email address already in use - ${data.email}`);
}
return user;
});
}
}
// No change to email:
return user;
});
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
data.avatar = gravatar.url(data.email || user.email, { default: "mm" });
return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));
})
.then(() => {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: { ...data, id: user.id, name: user.name },
})
.then(() => {
return user;
});
});
},
/**
* @param {Access} access
* @param {Object} [data]
* @param {Integer} [data.id] Defaults to the token user
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
if (typeof thisData.id === "undefined" || !thisData.id) {
thisData.id = access.token.getUserId(0);
}
return access
.can("users:get", thisData.id)
.then(() => {
const query = userModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[permissions]")
.first();
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
if (row.avatar === "") {
row.avatar = DEFAULT_AVATAR;
}
return row;
});
},
/**
* Checks if an email address is available, but if a user_id is supplied, it will ignore checking
* against that user.
*
* @param email
* @param user_id
*/
isEmailAvailable: (email, user_id) => {
const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first();
if (typeof user_id !== "undefined") {
query.where("id", "!=", user_id);
}
return query.then((user) => {
return !user;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("users:delete", data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (!user) {
throw new errs.ItemNotFoundError(data.id);
}
// Make sure user can't delete themselves
if (user.id === access.token.getUserId(0)) {
throw new errs.PermissionError("You cannot delete yourself.");
}
return userModel
.query()
.where("id", user.id)
.patch({
is_deleted: 1,
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "user",
object_id: user.id,
meta: _.omit(user, omissions()),
});
});
})
.then(() => {
return true;
});
},
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
},
/**
* This will only count the users
*
* @param {Access} access
* @param {String} [search_query]
* @returns {*}
*/
getCount: (access, search_query) => {
return access
.can("users:list")
.then(() => {
const query = userModel.query().count("id as count").where("is_deleted", 0).first();
// Query is used for searching
if (typeof search_query === "string") {
query.where(function () {
this.where("user.name", "like", `%${search_query}%`).orWhere(
"user.email",
"like",
`%${search_query}%`,
);
});
}
return query;
})
.then((row) => {
return Number.parseInt(row.count, 10);
});
},
/**
* All users
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, search_query) => {
await access.can("users:list");
const query = userModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[permissions]")
.orderBy("name", "ASC");
// Query is used for searching
if (typeof search_query === "string") {
query.where(function () {
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const res = await query;
return utils.omitRows(omissions())(res);
},
/**
* @param {Access} access
* @param {Integer} [id_requested]
* @returns {[String]}
*/
getUserOmisionsByAccess: (access, idRequested) => {
let response = []; // Admin response
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
response = ["is_deleted"]; // Restricted response
}
return response;
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} data.type
* @param {String} data.secret
* @return {Promise}
*/
setPassword: (access, data) => {
return access
.can("users:password", data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
if (user.id === access.token.getUserId(0)) {
// they're setting their own password. Make sure their current password is correct
if (typeof data.current === "undefined" || !data.current) {
throw new errs.ValidationError("Current password was not supplied");
}
return internalToken
.getTokenFromEmail({
identity: user.email,
secret: data.current,
})
.then(() => {
return user;
});
}
return user;
})
.then((user) => {
// Get auth, patch if it exists
return authModel
.query()
.where("user_id", user.id)
.andWhere("type", data.type)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret,
});
}
// insert
return authModel.query().insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
});
})
.then(() => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
password_changed: true,
auth_type: data.type,
},
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @return {Promise}
*/
setPermissions: (access, data) => {
return access
.can("users:permissions", data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
return user;
})
.then((user) => {
// Get perms row, patch if it exists
return userPermissionModel
.query()
.where("user_id", user.id)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
return userPermissionModel
.query()
.where("user_id", user.id)
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));
}
// insert
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
})
.then((permissions) => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
permissions: permissions,
},
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
*/
loginAs: (access, data) => {
return access
.can("users:loginas", data.id)
.then(() => {
return internalUser.get(access, data);
})
.then((user) => {
return internalToken.getTokenFromUser(user);
});
},
};
export default internalUser;
================================================
FILE: backend/knexfile.js
================================================
module.exports = {
development: {
client: 'mysql2',
migrations: {
tableName: 'migrations',
stub: 'lib/migrate_template.js',
directory: 'migrations'
}
},
production: {
client: 'mysql2',
migrations: {
tableName: 'migrations',
stub: 'lib/migrate_template.js',
directory: 'migrations'
}
}
};
================================================
FILE: backend/lib/access/access_lists-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_access_lists", "roles"],
"properties": {
"permission_access_lists": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/access_lists-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_access_lists", "roles"],
"properties": {
"permission_access_lists": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/access_lists-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_access_lists", "roles"],
"properties": {
"permission_access_lists": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/access_lists-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_access_lists", "roles"],
"properties": {
"permission_access_lists": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/access_lists-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_access_lists", "roles"],
"properties": {
"permission_access_lists": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/auditlog-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/certificates-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_certificates", "roles"],
"properties": {
"permission_certificates": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/certificates-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_certificates", "roles"],
"properties": {
"permission_certificates": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/certificates-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_certificates", "roles"],
"properties": {
"permission_certificates": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/certificates-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_certificates", "roles"],
"properties": {
"permission_certificates": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/certificates-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_certificates", "roles"],
"properties": {
"permission_certificates": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/dead_hosts-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_dead_hosts", "roles"],
"properties": {
"permission_dead_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/dead_hosts-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_dead_hosts", "roles"],
"properties": {
"permission_dead_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/dead_hosts-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_dead_hosts", "roles"],
"properties": {
"permission_dead_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/dead_hosts-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_dead_hosts", "roles"],
"properties": {
"permission_dead_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/dead_hosts-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_dead_hosts", "roles"],
"properties": {
"permission_dead_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/permissions.json
================================================
{
"$id": "perms",
"definitions": {
"view": {
"type": "string",
"pattern": "^(view|manage)$"
},
"manage": {
"type": "string",
"pattern": "^(manage)$"
}
}
}
================================================
FILE: backend/lib/access/proxy_hosts-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_proxy_hosts", "roles"],
"properties": {
"permission_proxy_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/proxy_hosts-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_proxy_hosts", "roles"],
"properties": {
"permission_proxy_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/proxy_hosts-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_proxy_hosts", "roles"],
"properties": {
"permission_proxy_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/proxy_hosts-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_proxy_hosts", "roles"],
"properties": {
"permission_proxy_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/proxy_hosts-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_proxy_hosts", "roles"],
"properties": {
"permission_proxy_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/redirection_hosts-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_redirection_hosts", "roles"],
"properties": {
"permission_redirection_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/redirection_hosts-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_redirection_hosts", "roles"],
"properties": {
"permission_redirection_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/redirection_hosts-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_redirection_hosts", "roles"],
"properties": {
"permission_redirection_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/redirection_hosts-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_redirection_hosts", "roles"],
"properties": {
"permission_redirection_hosts": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/redirection_hosts-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_redirection_hosts", "roles"],
"properties": {
"permission_redirection_hosts": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/reports-hosts.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/user"
}
]
}
================================================
FILE: backend/lib/access/roles.json
================================================
{
"$id": "roles",
"definitions": {
"admin": {
"type": "object",
"required": ["scope", "roles"],
"properties": {
"scope": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^user$"
}
},
"roles": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^admin$"
}
}
}
},
"user": {
"type": "object",
"required": ["scope"],
"properties": {
"scope": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^user$"
}
}
}
}
}
}
================================================
FILE: backend/lib/access/settings-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/settings-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/settings-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/streams-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_streams", "roles"],
"properties": {
"permission_streams": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/streams-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_streams", "roles"],
"properties": {
"permission_streams": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/streams-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_streams", "roles"],
"properties": {
"permission_streams": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/streams-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_streams", "roles"],
"properties": {
"permission_streams": {
"$ref": "perms#/definitions/view"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/streams-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["permission_streams", "roles"],
"properties": {
"permission_streams": {
"$ref": "perms#/definitions/manage"
},
"roles": {
"type": "array",
"items": {
"type": "string",
"enum": ["user"]
}
}
}
}
]
}
================================================
FILE: backend/lib/access/users-create.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/users-delete.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/users-get.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["data", "scope"],
"properties": {
"data": {
"$ref": "objects#/properties/users"
},
"scope": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^user$"
}
}
}
}
]
}
================================================
FILE: backend/lib/access/users-list.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/users-loginas.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/users-password.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["data", "scope"],
"properties": {
"data": {
"$ref": "objects#/properties/users"
},
"scope": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^user$"
}
}
}
}
]
}
================================================
FILE: backend/lib/access/users-permissions.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
================================================
FILE: backend/lib/access/users-update.json
================================================
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
},
{
"type": "object",
"required": ["data", "scope"],
"properties": {
"data": {
"$ref": "objects#/properties/users"
},
"scope": {
"type": "array",
"contains": {
"type": "string",
"pattern": "^user$"
}
}
}
}
]
}
================================================
FILE: backend/lib/access.js
================================================
/**
* Some Notes: This is a friggin complicated piece of code.
*
* "scope" in this file means "where did this token come from and what is using it", so 99% of the time
* the "scope" is going to be "user" because it would be a user token. This is not to be confused with
* the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
*/
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import { access as logger } from "../logger.js";
import proxyHostModel from "../models/proxy_host.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import permsSchema from "./access/permissions.json" with { type: "json" };
import roleSchema from "./access/roles.json" with { type: "json" };
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default function (tokenString) {
const Token = TokenModel();
let tokenData = null;
let initialised = false;
const objectCache = {};
let allowInternalAccess = false;
let userRoles = [];
let permissions = {};
/**
* Loads the Token object from the token string
*
* @returns {Promise}
*/
this.init = async () => {
if (initialised) {
return;
}
if (!tokenString) {
throw new errs.PermissionError("Permission Denied");
}
tokenData = await Token.load(tokenString);
// At this point we need to load the user from the DB and make sure they:
// - exist (and not soft deleted)
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (
tokenData.attrs.id ||
(typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1)
) {
// Has token user id or token user scope
const user = await userModel
.query()
.where("id", tokenData.attrs.id)
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.allowGraph("[permissions]")
.withGraphFetched("[permissions]")
.first();
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push("user");
let ok = true;
_.forEach(tokenData.scope, (scope_item) => {
if (_.indexOf(user.roles, scope_item) === -1) {
ok = false;
}
});
if (!ok) {
throw new errs.AuthError("Invalid token scope for User");
}
initialised = true;
userRoles = user.roles;
permissions = user.permissions;
} else {
throw new errs.AuthError("User cannot be loaded for Token");
}
}
initialised = true;
};
/**
* Fetches the object ids from the database, only once per object type, for this token.
* This only applies to USER token scopes, as all other tokens are not really bound
* by object scopes
*
* @param {String} objectType
* @returns {Promise}
*/
this.loadObjects = async (objectType) => {
let objects = null;
if (Token.hasScope("user")) {
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) {
throw new errs.AuthError("User Token supplied without a User ID");
}
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0;
if (typeof objectCache[objectType] !== "undefined") {
objects = objectCache[objectType];
} else {
switch (objectType) {
// USERS - should only return yourself
case "users":
objects = tokenUserId ? [tokenUserId] : [];
break;
// Proxy Hosts
case "proxy_hosts": {
const query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
if (permissions.visibility === "user") {
query.andWhere("owner_user_id", tokenUserId);
}
const rows = await query;
objects = [];
_.forEach(rows, (ruleRow) => {
objects.push(ruleRow.id);
});
// enum should not have less than 1 item
if (!objects.length) {
objects.push(0);
}
break;
}
}
objectCache[objectType] = objects;
}
}
return objects;
};
/**
* Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
*
* @param {String} permissionLabel
* @returns {Object}
*/
this.getObjectSchema = async (permissionLabel) => {
const baseObjectType = permissionLabel.split(":").shift();
const schema = {
$id: "objects",
description: "Actor Properties",
type: "object",
additionalProperties: false,
properties: {
user_id: {
anyOf: [
{
type: "number",
enum: [Token.get("attrs").id],
},
],
},
scope: {
type: "string",
pattern: `^${Token.get("scope")}$`,
},
},
};
const result = await this.loadObjects(baseObjectType);
if (typeof result === "object" && result !== null) {
schema.properties[baseObjectType] = {
type: "number",
enum: result,
minimum: 1,
};
} else {
schema.properties[baseObjectType] = {
type: "number",
minimum: 1,
};
}
return schema;
};
// here:
return {
token: Token,
/**
*
* @param {Boolean} [allowInternal]
* @returns {Promise}
*/
load: async (allowInternal) => {
if (tokenString) {
return await Token.load(tokenString);
}
allowInternalAccess = allowInternal;
return allowInternal || null;
},
reloadObjects: this.loadObjects,
/**
*
* @param {String} permission
* @param {*} [data]
* @returns {Promise}
*/
can: async (permission, data) => {
if (allowInternalAccess === true) {
return true;
}
try {
await this.init();
const objectSchema = await this.getObjectSchema(permission);
const dataSchema = {
[permission]: {
data: data,
scope: Token.get("scope"),
roles: userRoles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
},
};
const permissionSchema = {
$async: true,
$id: "permissions",
type: "object",
additionalProperties: false,
properties: {},
};
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, {
encoding: "utf8",
});
permissionSchema.properties[permission] = JSON.parse(rawData);
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
});
const valid = await ajv.validate("permissions", dataSchema);
return valid && dataSchema[permission];
} catch (err) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
}
},
};
}
================================================
FILE: backend/lib/certbot.js
================================================
import batchflow from "batchflow";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
import { certbot as logger } from "../logger.js";
import errs from "./error.js";
import utils from "./utils.js";
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
/**
* Installs a cerbot plugin given the key for the object from
* ../certbot/dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
const installPlugin = async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === "undefined") {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new errs.ItemNotFoundError(pluginKey);
}
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
// in new versions of Python
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "stdlib" });
if (typeof plugin.env === "object") {
env = Object.assign(env, plugin.env);
}
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
return utils
.exec(cmd, { env })
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
};
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
});
});
};
export { installPlugins, installPlugin };
================================================
FILE: backend/lib/config.js
================================================
import fs from "node:fs";
import NodeRSA from "node-rsa";
import { global as logger } from "../logger.js";
const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'better-sqlite3';
// Not used for new setups anymore but may exist in legacy setups
const legacySqliteClientName = 'sqlite3';
let instance = null;
// 1. Load from config file first (not recommended anymore)
// 2. Use config env variables next
const configure = () => {
const filename = `${process.env.NODE_CONFIG_DIR || "./config"}/${process.env.NODE_ENV || "default"}.json`;
if (fs.existsSync(filename)) {
let configData;
try {
// Load this json synchronously
const rawData = fs.readFileSync(filename);
configData = JSON.parse(rawData);
} catch (_) {
// do nothing
}
if (configData?.database) {
logger.info(`Using configuration from file: ${filename}`);
// Migrate those who have "mysql" engine to "mysql2"
if (configData.database.engine === "mysql") {
configData.database.engine = mysqlEngine;
}
instance = configData;
instance.keys = getKeys();
return;
}
}
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim());
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration");
instance = {
database: {
engine: mysqlEngine,
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,
},
keys: getKeys(),
};
return;
}
const envPostgresHost = process.env.DB_POSTGRES_HOST || null;
const envPostgresUser = process.env.DB_POSTGRES_USER || null;
const envPostgresName = process.env.DB_POSTGRES_NAME || null;
if (envPostgresHost && envPostgresUser && envPostgresName) {
// we have enough postgres creds to go with postgres
logger.info("Using Postgres configuration");
instance = {
database: {
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
password: process.env.DB_POSTGRES_PASSWORD,
name: envPostgresName,
},
keys: getKeys(),
};
return;
}
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
engine: "knex-native",
knex: {
client: sqliteClientName,
connection: {
filename: envSqliteFile,
},
useNullAsDefault: true,
},
},
keys: getKeys(),
};
};
const getKeys = () => {
// Get keys from file
if (isDebugMode()) {
logger.debug("Checking for keys file:", keysFile);
}
if (!fs.existsSync(keysFile)) {
generateKeys();
} else if (process.env.DEBUG) {
logger.info("Keys file exists OK");
}
try {
// Load this json keysFile synchronously and return the json object
const rawData = fs.readFileSync(keysFile);
return JSON.parse(rawData);
} catch (err) {
logger.error(`Could not read JWT key pair from config file: ${keysFile}`, err);
process.exit(1);
}
};
const generateKeys = () => {
logger.info("Creating a new JWT key pair...");
// Now create the keys and save them in the config.
const key = new NodeRSA({ b: 2048 });
key.generateKeyPair();
const keys = {
key: key.exportKey("private").toString(),
pub: key.exportKey("public").toString(),
};
// Write keys config
try {
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
} catch (err) {
logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`);
process.exit(1);
}
logger.info(`Wrote JWT key pair to config file: ${keysFile}`);
};
/**
*
* @param {string} key ie: 'database' or 'database.engine'
* @returns {boolean}
*/
const configHas = (key) => {
instance === null && configure();
const keys = key.split(".");
let level = instance;
let has = true;
keys.forEach((keyItem) => {
if (typeof level[keyItem] === "undefined") {
has = false;
} else {
level = level[keyItem];
}
});
return has;
};
/**
* Gets a specific key from the top level
*
* @param {string} key
* @returns {*}
*/
const configGet = (key) => {
instance === null && configure();
if (key && typeof instance[key] !== "undefined") {
return instance[key];
}
return instance;
};
/**
* Is this a sqlite configuration?
*
* @returns {boolean}
*/
const isSqlite = () => {
instance === null && configure();
return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);
};
/**
* Is this a mysql configuration?
*
* @returns {boolean}
*/
const isMysql = () => {
instance === null && configure();
return instance.database.engine === mysqlEngine;
};
/**
* Is this a postgres configuration?
*
* @returns {boolean}
*/
const isPostgres = () => {
instance === null && configure();
return instance.database.engine === postgresEngine;
};
/**
* Are we running in debug mdoe?
*
* @returns {boolean}
*/
const isDebugMode = () => !!process.env.DEBUG;
/**
* Are we running in CI?
*
* @returns {boolean}
*/
const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';
/**
* Returns a public key
*
* @returns {string}
*/
const getPublicKey = () => {
instance === null && configure();
return instance.keys.pub;
};
/**
* Returns a private key
*
* @returns {string}
*/
const getPrivateKey = () => {
instance === null && configure();
return instance.keys.key;
};
/**
* @returns {boolean}
*/
const useLetsencryptStaging = () => !!process.env.LE_STAGING;
/**
* @returns {string|null}
*/
const useLetsencryptServer = () => {
if (process.env.LE_SERVER) {
return process.env.LE_SERVER;
}
return null;
};
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };
================================================
FILE: backend/lib/error.js
================================================
import _ from "lodash";
const errs = {
PermissionError: function (_, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = "Permission Denied";
this.public = true;
this.status = 403;
},
ItemNotFoundError: function (id, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = "Not Found";
if (id) {
this.message = `Not Found - ${id}`;
}
this.public = true;
this.status = 404;
},
AuthError: function (message, messageI18n, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.message_i18n = messageI18n;
this.public = true;
this.status = 400;
},
InternalError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 500;
this.public = false;
},
InternalValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = false;
},
ConfigurationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = true;
},
CacheError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.previous = previous;
this.status = 500;
this.public = false;
},
ValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = true;
this.status = 400;
},
AssertionFailedError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = false;
this.status = 400;
},
CommandError: function (stdErr, code, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = stdErr;
this.code = code;
this.public = false;
},
};
_.forEach(errs, (err) => {
err.prototype = Object.create(Error.prototype);
});
export default errs;
================================================
FILE: backend/lib/express/cors.js
================================================
export default (req, res, next) => {
if (req.headers.origin) {
res.set({
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Methods": "OPTIONS, GET, POST",
"Access-Control-Allow-Headers":
"Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
"Access-Control-Max-Age": 5 * 60,
"Access-Control-Expose-Headers": "X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
});
next();
} else {
// No origin
next();
}
};
================================================
FILE: backend/lib/express/jwt-decode.js
================================================
import Access from "../access.js";
export default () => {
return async (_, res, next) => {
try {
res.locals.access = null;
const access = new Access(res.locals.token || null);
await access.load();
res.locals.access = access;
next();
} catch (err) {
next(err);
}
};
};
================================================
FILE: backend/lib/express/jwt.js
================================================
export default function () {
return (req, res, next) => {
if (req.headers.authorization) {
const parts = req.headers.authorization.split(" ");
if (parts && parts[0] === "Bearer" && parts[1]) {
res.locals.token = parts[1];
}
}
next();
};
}
================================================
FILE: backend/lib/express/pagination.js
================================================
import _ from "lodash";
export default (default_sort, default_offset, default_limit, max_limit) => {
/**
* This will setup the req query params with filtered data and defaults
*
* sort will be an array of fields and their direction
* offset will be an int, defaulting to zero if no other default supplied
* limit will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied
*
*/
return (req, _res, next) => {
req.query.offset =
typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10);
req.query.limit =
typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);
if (max_limit && req.query.limit > max_limit) {
req.query.limit = max_limit;
}
// Sorting
let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort;
const myRegexp = /.*\.(asc|desc)$/gi;
const sort_array = [];
sort = sort.split(",");
_.map(sort, (val) => {
const matches = myRegexp.exec(val);
if (matches !== null) {
const dir = matches[1];
sort_array.push({
field: val.substr(0, val.length - (dir.length + 1)),
dir: dir.toLowerCase(),
});
} else {
sort_array.push({
field: val,
dir: "asc",
});
}
});
// Sort will now be in this format:
// [
// { field: 'field1', dir: 'asc' },
// { field: 'field2', dir: 'desc' }
// ]
req.query.sort = sort_array;
next();
};
};
================================================
FILE: backend/lib/express/user-id-from-me.js
================================================
export default (req, res, next) => {
if (req.params.user_id === 'me' && res.locals.access) {
req.params.user_id = res.locals.access.token.get('attrs').id;
} else {
req.params.user_id = Number.parseInt(req.params.user_id, 10);
}
next();
};
================================================
FILE: backend/lib/helpers.js
================================================
import moment from "moment";
import { ref } from "objection";
import { isPostgres } from "./config.js";
/**
* Takes an expression such as 30d and returns a moment object of that date in future
*
* Key Shorthand
* ==================
* years y
* quarters Q
* months M
* weeks w
* days d
* hours h
* minutes m
* seconds s
* milliseconds ms
*
* @param {String} expression
* @returns {Object}
*/
const parseDatePeriod = (expression) => {
const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
}
return null;
};
const convertIntFieldsToBool = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] === 1;
}
});
return obj;
};
const convertBoolFieldsToInt = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] ? 1 : 0;
}
});
return obj;
};
/**
* Casts a column to json if using postgres
*
* @param {string} colName
* @returns {string|Objection.ReferenceBuilder}
*/
const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName);
export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed };
================================================
FILE: backend/lib/migrate_template.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "identifier_for_migrate";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (_knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
// Create Table example:
/*
return knex.schema.createTable('notification', (table) => {
table.increments().primary();
table.string('name').notNull();
table.string('type').notNull();
table.integer('created_on').notNull();
table.integer('modified_on').notNull();
})
.then(function () {
logger.info('[' + migrateName + '] Notification Table created');
});
*/
logger.info(`[${migrateName}] Migrating Up Complete`);
return Promise.resolve(true);
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
// Drop table example:
/*
return knex.schema.dropTable('notification')
.then(() => {
logger.info(`[${migrateName}] Notification Table dropped`);
});
*/
logger.info(`[${migrateName}] Migrating Down Complete`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/lib/utils.js
================================================
import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { Liquid } from "liquidjs";
import _ from "lodash";
import { debug, global as logger } from "../logger.js";
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const exec = async (cmd, options = {}) => {
debug(logger, "CMD:", cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = nodeExec(cmd, options, (isError, stdout, stderr) => {
if (isError) {
reject(new errs.CommandError(stderr, isError));
} else {
resolve({ stdout, stderr });
}
});
child.on("error", (e) => {
reject(new errs.CommandError(stderr, 1, e));
});
});
return stdout;
};
/**
* @param {String} cmd
* @param {Array} args
* @param {Object|undefined} options
* @returns {Promise}
*/
const execFile = (cmd, args, options) => {
debug(logger, `CMD: ${cmd} ${args ? args.join(" ") : ""}`);
const opts = options || {};
return new Promise((resolve, reject) => {
nodeExecFile(cmd, args, opts, (err, stdout, stderr) => {
if (err && typeof err === "object") {
reject(new errs.CommandError(stderr, 1, err));
} else {
resolve(stdout.trim());
}
});
});
};
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
const omitRow = (omissions) => {
/**
* @param {Object} row
* @returns {Object}
*/
return (row) => {
return _.omit(row, omissions);
};
};
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
const omitRows = (omissions) => {
/**
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
});
return rows;
};
};
/**
* @returns {Object} Liquid render engine
*/
const getRenderEngine = () => {
const renderEngine = new Liquid({
root: `${__dirname}/../templates/`,
});
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
*/
renderEngine.registerFilter("nginxAccessRule", (v) => {
if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) {
return `${v.directive} ${v.address};`;
}
return "";
});
return renderEngine;
};
export default { exec, execFile, omitRow, omitRows, getRenderEngine };
================================================
FILE: backend/lib/validator/api.js
================================================
import Ajv from "ajv/dist/2020.js";
import errs from "../error.js";
const ajv = new Ajv({
verbose: true,
allErrors: true,
allowUnionTypes: true,
strict: false,
coerceTypes: true,
});
/**
* @param {Object} schema
* @param {Object} payload
* @returns {Promise}
*/
const apiValidator = async (schema, payload /*, description*/) => {
if (!schema) {
throw new errs.ValidationError("Schema is undefined");
}
// Can't use falsy check here as valid payload could be `0` or `false`
if (typeof payload === "undefined") {
throw new errs.ValidationError("Payload is undefined");
}
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
return payload;
}
const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message);
err.debug = {validationErrors: validate.errors, payload};
throw err;
};
export default apiValidator;
================================================
FILE: backend/lib/validator/index.js
================================================
import Ajv from 'ajv/dist/2020.js';
import _ from "lodash";
import commonDefinitions from "../../schema/common.json" with { type: "json" };
import errs from "../error.js";
RegExp.prototype.toJSON = RegExp.prototype.toString;
const ajv = new Ajv({
verbose: true,
allErrors: true,
allowUnionTypes: true,
coerceTypes: true,
strict: false,
schemas: [commonDefinitions],
});
/**
*
* @param {Object} schema
* @param {Object} payload
* @returns {Promise}
*/
const validator = (schema, payload) => {
return new Promise((resolve, reject) => {
if (!payload) {
reject(new errs.InternalValidationError("Payload is falsy"));
} else {
try {
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
resolve(_.cloneDeep(payload));
} else {
const message = ajv.errorsText(validate.errors);
reject(new errs.InternalValidationError(message));
}
} catch (err) {
reject(err);
}
}
});
};
export default validator;
================================================
FILE: backend/logger.js
================================================
import signale from "signale";
import { isDebugMode } from "./lib/config.js";
const opts = {
logLevel: "info",
};
const global = new signale.Signale({ scope: "Global ", ...opts });
const migrate = new signale.Signale({ scope: "Migrate ", ...opts });
const express = new signale.Signale({ scope: "Express ", ...opts });
const access = new signale.Signale({ scope: "Access ", ...opts });
const nginx = new signale.Signale({ scope: "Nginx ", ...opts });
const ssl = new signale.Signale({ scope: "SSL ", ...opts });
const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
const debug = (logger, ...args) => {
if (isDebugMode()) {
logger.debug(...args);
}
};
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
================================================
FILE: backend/migrate.js
================================================
import db from "./db.js";
import { migrate as logger } from "./logger.js";
const migrateUp = async () => {
const version = await db().migrate.currentVersion();
logger.info("Current database version:", version);
return await db().migrate.latest({
tableName: "migrations",
directory: "migrations",
});
};
export { migrateUp };
================================================
FILE: backend/migrations/20180618015850_initial.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "initial-schema";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.createTable("auth", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("type", 30).notNull();
table.string("secret").notNull();
table.json("meta").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] auth Table created`);
return knex.schema.createTable("user", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("is_disabled").notNull().unsigned().defaultTo(0);
table.string("email").notNull();
table.string("name").notNull();
table.string("nickname").notNull();
table.string("avatar").notNull();
table.json("roles").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] user Table created`);
return knex.schema.createTable("user_permission", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("visibility").notNull();
table.string("proxy_hosts").notNull();
table.string("redirection_hosts").notNull();
table.string("dead_hosts").notNull();
table.string("streams").notNull();
table.string("access_lists").notNull();
table.string("certificates").notNull();
table.unique("user_id");
});
})
.then(() => {
logger.info(`[${migrateName}] user_permission Table created`);
return knex.schema.createTable("proxy_host", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_ip").notNull();
table.integer("forward_port").notNull().unsigned();
table.integer("access_list_id").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("caching_enabled").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table created`);
return knex.schema.createTable("redirection_host", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_domain_name").notNull();
table.integer("preserve_path").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table created`);
return knex.schema.createTable("dead_host", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table created`);
return knex.schema.createTable("stream", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("incoming_port").notNull().unsigned();
table.string("forward_ip").notNull();
table.integer("forwarding_port").notNull().unsigned();
table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0);
table.integer("udp_forwarding").notNull().unsigned().defaultTo(0);
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] stream Table created`);
return knex.schema.createTable("access_list", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("name").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] access_list Table created`);
return knex.schema.createTable("certificate", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("provider").notNull();
table.string("nice_name").notNull().defaultTo("");
table.json("domain_names").notNull();
table.dateTime("expires_on").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] certificate Table created`);
return knex.schema.createTable("access_list_auth", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("username").notNull();
table.string("password").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] access_list_auth Table created`);
return knex.schema.createTable("audit_log", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("object_type").notNull().defaultTo("");
table.integer("object_id").notNull().unsigned().defaultTo(0);
table.string("action").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] audit_log Table created`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20180929054513_websockets.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "websockets";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20181019052346_forward_host.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "forward_host";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.renameColumn("forward_ip", "forward_host");
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20181113041458_http2_support.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "http2_support";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("http2_support").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20181213013211_forward_scheme.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "forward_scheme";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.string("forward_scheme").notNull().defaultTo("http");
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20190104035154_disabled.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "disabled";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("enabled").notNull().unsigned().defaultTo(1);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table altered`);
return knex.schema.table("stream", (stream) => {
stream.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20190215115310_customlocations.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "custom_locations";
/**
* Migrate
* Extends proxy_host table with locations field
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.json("locations");
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20190218060101_hsts.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "hsts";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20190227065017_settings.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "settings";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.createTable('setting', (table) => {
table.string('id').notNull().primary();
table.string('name', 100).notNull();
table.string('description', 255).notNull();
table.string('value', 255).notNull();
table.json('meta').notNull();
})
.then(() => {
logger.info(`[${migrateName}] setting Table created`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20200410143839_access_list_client.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "access_list_client";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.createTable("access_list_client", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("address").notNull();
table.string("directive").notNull();
table.json("meta").notNull();
})
.then(() => {
logger.info(`[${migrateName}] access_list_client Table created`);
return knex.schema.table("access_list", (access_list) => {
access_list.integer("satify_any").notNull().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] access_list Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.dropTable("access_list_client").then(() => {
logger.info(`[${migrateName}] access_list_client Table dropped`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20200410143840_access_list_client_fix.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "access_list_client_fix";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("access_list", (access_list) => {
access_list.renameColumn("satify_any", "satisfy_any");
})
.then(() => {
logger.info(`[${migrateName}] access_list Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
================================================
FILE: backend/migrations/20201014143841_pass_auth.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "pass_auth";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("access_list", (access_list) => {
access_list.integer("pass_auth").notNull().defaultTo(1);
})
.then(() => {
logger.info(`[${migrateName}] access_list Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("access_list", (access_list) => {
access_list.dropColumn("pass_auth");
})
.then(() => {
logger.info(`[${migrateName}] access_list pass_auth Column dropped`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20210210154702_redirection_scheme.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "redirection_scheme";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("redirection_host", (table) => {
table.string("forward_scheme").notNull().defaultTo("$scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("redirection_host", (table) => {
table.dropColumn("forward_scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20210210154703_redirection_status_code.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "redirection_status_code";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("redirection_host", (table) => {
table.integer("forward_http_code").notNull().unsigned().defaultTo(302);
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("redirection_host", (table) => {
table.dropColumn("forward_http_code");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20210423103500_stream_domain.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "stream_domain";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("stream", (table) => {
table.renameColumn("forward_ip", "forwarding_host");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("stream", (table) => {
table.renameColumn("forwarding_host", "forward_ip");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20211108145214_regenerate_default_host.js
================================================
import internalNginx from "../internal/nginx.js";
import { migrate as logger } from "../logger.js";
const migrateName = "stream_domain";
async function regenerateDefaultHost(knex) {
const row = await knex("setting").select("*").where("id", "default-site").first();
if (!row) {
return Promise.resolve();
}
return internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.generateConfig("default", row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
});
}
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return regenerateDefaultHost(knex);
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return regenerateDefaultHost(knex);
};
export { up, down };
================================================
FILE: backend/migrations/20240427161436_stream_ssl.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "stream_ssl";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("stream", (table) => {
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("stream", (table) => {
table.dropColumn("certificate_id");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20251111090000_redirect_auto_scheme.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "redirect_auto_scheme";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("redirection_host", async (table) => {
// change the column default from $scheme to auto
await table.string("forward_scheme").notNull().defaultTo("auto").alter();
await knex('redirection_host')
.where('forward_scheme', '$scheme')
.update({ forward_scheme: 'auto' });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("redirection_host", async (table) => {
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
await knex('redirection_host')
.where('forward_scheme', 'auto')
.update({ forward_scheme: '$scheme' });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
================================================
FILE: backend/migrations/20260131163528_trust_forwarded_proto.js
================================================
import { migrate as logger } from "../logger.js";
const migrateName = "trust_forwarded_proto";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = function (knex) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.alterTable('proxy_host', (table) => {
table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = function (knex) {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.alterTable('proxy_host', (table) => {
table.dropColumn('trust_forwarded_proto');
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
export { up, down };
================================================
FILE: backend/models/access_list.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessListAuth from "./access_list_auth.js";
import AccessListClient from "./access_list_client.js";
import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
Model.knex(db());
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
class AccessList extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "AccessList";
}
static get tableName() {
return "access_list";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "access_list.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
items: {
relation: Model.HasManyRelation,
modelClass: AccessListAuth,
join: {
from: "access_list.id",
to: "access_list_auth.access_list_id",
},
},
clients: {
relation: Model.HasManyRelation,
modelClass: AccessListClient,
join: {
from: "access_list.id",
to: "access_list_client.access_list_id",
},
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: ProxyHostModel,
join: {
from: "access_list.id",
to: "proxy_host.access_list_id",
},
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
};
}
}
export default AccessList;
================================================
FILE: backend/models/access_list_auth.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db());
class AccessListAuth extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate() {
this.modified_on = now();
}
static get name() {
return "AccessListAuth";
}
static get tableName() {
return "access_list_auth";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_auth.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
};
}
}
export default AccessListAuth;
================================================
FILE: backend/models/access_list_client.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db());
class AccessListClient extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate() {
this.modified_on = now();
}
static get name() {
return "AccessListClient";
}
static get tableName() {
return "access_list_client";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_client.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
};
}
}
export default AccessListClient;
================================================
FILE: backend/models/audit-log.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
class AuditLog extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate() {
this.modified_on = now();
}
static get name() {
return "AuditLog";
}
static get tableName() {
return "audit_log";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
user: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "audit_log.user_id",
to: "user.id",
},
},
};
}
}
export default AuditLog;
================================================
FILE: backend/models/auth.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import bcrypt from "bcrypt";
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
const boolFields = ["is_deleted"];
function encryptPassword() {
if (this.type === "password" && this.secret) {
return bcrypt.hash(this.secret, 13).then((hash) => {
this.secret = hash;
});
}
return null;
}
class Auth extends Model {
$beforeInsert(queryContext) {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
return encryptPassword.apply(this, queryContext);
}
$beforeUpdate(queryContext) {
this.modified_on = now();
return encryptPassword.apply(this, queryContext);
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
/**
* Verify a plain password against the encrypted password
*
* @param {String} password
* @returns {Promise}
*/
verifyPassword(password) {
return bcrypt.compare(password, this.secret);
}
static get name() {
return "Auth";
}
static get tableName() {
return "auth";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
user: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "auth.user_id",
to: "user.id",
},
filter: {
is_deleted: 0,
},
},
};
}
}
export default Auth;
================================================
FILE: backend/models/certificate.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import deadHostModel from "./dead_host.js";
import now from "./now_helper.js";
import proxyHostModel from "./proxy_host.js";
import redirectionHostModel from "./redirection_host.js";
import streamModel from "./stream.js";
import userModel from "./user.js";
Model.knex(db());
const boolFields = ["is_deleted"];
class Certificate extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for expires_on
if (typeof this.expires_on === "undefined") {
this.expires_on = now();
}
// Default for domain_names
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "Certificate";
}
static get tableName() {
return "certificate";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: userModel,
join: {
from: "certificate.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: proxyHostModel,
join: {
from: "certificate.id",
to: "proxy_host.certificate_id",
},
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
dead_hosts: {
relation: Model.HasManyRelation,
modelClass: deadHostModel,
join: {
from: "certificate.id",
to: "dead_host.certificate_id",
},
modify: (qb) => {
qb.where("dead_host.is_deleted", 0);
},
},
redirection_hosts: {
relation: Model.HasManyRelation,
modelClass: redirectionHostModel,
join: {
from: "certificate.id",
to: "redirection_host.certificate_id",
},
modify: (qb) => {
qb.where("redirection_host.is_deleted", 0);
},
},
streams: {
relation: Model.HasManyRelation,
modelClass: streamModel,
join: {
from: "certificate.id",
to: "stream.certificate_id",
},
modify: (qb) => {
qb.where("stream.is_deleted", 0);
},
},
};
}
}
export default Certificate;
================================================
FILE: backend/models/dead_host.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
class DeadHost extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "DeadHost";
}
static get tableName() {
return "dead_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "dead_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "dead_host.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
export default DeadHost;
================================================
FILE: backend/models/now_helper.js
================================================
import { Model } from "objection";
import db from "../db.js";
import { isSqlite } from "../lib/config.js";
Model.knex(db());
export default () => {
if (isSqlite()) {
return Model.raw("datetime('now','localtime')");
}
return Model.raw("NOW()");
};
================================================
FILE: backend/models/proxy_host.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessList from "./access_list.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
const boolFields = [
"is_deleted",
"ssl_forced",
"caching_enabled",
"block_exploits",
"allow_websocket_upgrade",
"http2_support",
"enabled",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto",
];
class ProxyHost extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "ProxyHost";
}
static get tableName() {
return "proxy_host";
}
static get jsonAttributes() {
return ["domain_names", "meta", "locations"];
}
static get defaultAllowGraph() {
return "[owner,access_list.[clients,items],certificate]";
}
static get defaultExpand() {
return ["owner", "certificate", "access_list.[clients,items]"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "proxy_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
access_list: {
relation: Model.HasOneRelation,
modelClass: AccessList,
join: {
from: "proxy_host.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "proxy_host.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
export default ProxyHost;
================================================
FILE: backend/models/redirection_host.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
const boolFields = [
"is_deleted",
"enabled",
"preserve_path",
"ssl_forced",
"block_exploits",
"hsts_enabled",
"hsts_subdomains",
"http2_support",
];
class RedirectionHost extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "RedirectionHost";
}
static get tableName() {
return "redirection_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "redirection_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "redirection_host.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
export default RedirectionHost;
================================================
FILE: backend/models/setting.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
Model.knex(db());
class Setting extends Model {
$beforeInsert () {
// Default for meta
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
static get name () {
return 'Setting';
}
static get tableName () {
return 'setting';
}
static get jsonAttributes () {
return ['meta'];
}
}
export default Setting;
================================================
FILE: backend/models/stream.js
================================================
import { Model } from "objection";
import db from "../db.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db());
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
class Stream extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "Stream";
}
static get tableName() {
return "stream";
}
static get jsonAttributes() {
return ["meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("incoming_port"), "ASC"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "stream.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "stream.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
export default Stream;
================================================
FILE: backend/models/token.js
================================================
/**
NOTE: This is not a database table, this is a model of a Token object that can be created/loaded
and then has abilities after that.
*/
import crypto from "node:crypto";
import jwt from "jsonwebtoken";
import _ from "lodash";
import { getPrivateKey, getPublicKey } from "../lib/config.js";
import errs from "../lib/error.js";
import { global as logger } from "../logger.js";
const ALGO = "RS256";
export default () => {
let tokenData = {};
const self = {
/**
* @param {Object} payload
* @returns {Promise}
*/
create: (payload) => {
if (!getPrivateKey()) {
logger.error("Private key is empty!");
}
// sign with RSA SHA256
const options = {
algorithm: ALGO,
expiresIn: payload.expiresIn || "1d",
};
payload.jti = crypto.randomBytes(12).toString("base64").substring(-8);
return new Promise((resolve, reject) => {
jwt.sign(payload, getPrivateKey(), options, (err, token) => {
if (err) {
reject(err);
} else {
tokenData = payload;
resolve({
token: token,
payload: payload,
});
}
});
});
},
/**
* @param {String} token
* @returns {Promise}
*/
load: (token) => {
if (!getPublicKey()) {
logger.error("Public key is empty!");
}
return new Promise((resolve, reject) => {
try {
if (!token || token === null || token === "null") {
reject(new errs.AuthError("Empty token"));
} else {
jwt.verify(
token,
getPublicKey(),
{ ignoreExpiration: false, algorithms: [ALGO] },
(err, result) => {
if (err) {
if (err.name === "TokenExpiredError") {
reject(new errs.AuthError("Token has expired", err));
} else {
reject(err);
}
} else {
tokenData = result;
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if (
typeof tokenData.scope !== "undefined" &&
_.indexOf(tokenData.scope, "all") !== -1
) {
tokenData.scope = ["user"];
}
resolve(tokenData);
}
},
);
}
} catch (err) {
reject(err);
}
});
},
/**
* Does the token have the specified scope?
*
* @param {String} scope
* @returns {Boolean}
*/
hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1,
/**
* @param {String} key
* @return {*}
*/
get: (key) => {
if (typeof tokenData[key] !== "undefined") {
return tokenData[key];
}
return null;
},
/**
* @param {String} key
* @param {*} value
*/
set: (key, value) => {
tokenData[key] = value;
},
/**
* @param [defaultValue]
* @returns {Integer}
*/
getUserId: (defaultValue) => {
const attrs = self.get("attrs");
if (attrs?.id) {
return attrs.id;
}
return defaultValue || 0;
},
};
return self;
};
================================================
FILE: backend/models/user.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import now from "./now_helper.js";
import UserPermission from "./user_permission.js";
Model.knex(db());
const boolFields = ["is_deleted", "is_disabled"];
class User extends Model {
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for roles
if (typeof this.roles === "undefined") {
this.roles = [];
}
}
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name() {
return "User";
}
static get tableName() {
return "user";
}
static get jsonAttributes() {
return ["roles"];
}
static get relationMappings() {
return {
permissions: {
relation: Model.HasOneRelation,
modelClass: UserPermission,
join: {
from: "user.id",
to: "user_permission.user_id",
},
},
};
}
}
export default User;
================================================
FILE: backend/models/user_permission.js
================================================
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import now from "./now_helper.js";
Model.knex(db());
class UserPermission extends Model {
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
}
$beforeUpdate () {
this.modified_on = now();
}
static get name () {
return 'UserPermission';
}
static get tableName () {
return 'user_permission';
}
}
export default UserPermission;
================================================
FILE: backend/nodemon.json
================================================
{
"verbose": false,
"ignore": [
"data"
],
"ext": "js json ejs cjs"
}
================================================
FILE: backend/package.json
================================================
{
"name": "nginx-proxy-manager",
"version": "2.0.0",
"description": "A beautiful interface for creating Nginx endpoints",
"author": "Jamie Curnow ",
"license": "MIT",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js",
"regenerate-config": "node scripts/regenerate-config"
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^15.3.1",
"ajv": "^8.18.0",
"archiver": "^7.0.1",
"batchflow": "^0.4.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.6.2",
"body-parser": "^2.2.2",
"compression": "^1.8.1",
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.3",
"knex": "3.1.0",
"liquidjs": "10.24.0",
"lodash": "^4.17.23",
"moment": "^2.30.1",
"mysql2": "^3.18.2",
"node-rsa": "^1.1.1",
"objection": "3.1.5",
"otplib": "^13.3.0",
"path": "^0.12.7",
"pg": "^8.19.0",
"proxy-agent": "^6.5.0",
"signale": "1.4.0",
"sqlite3": "^5.1.7",
"temp-write": "^6.0.1"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0",
"@biomejs/biome": "^2.4.5",
"chalk": "5.6.2",
"nodemon": "^3.1.14"
},
"signale": {
"displayDate": true,
"displayTimestamp": true
}
}
================================================
FILE: backend/routes/audit-log.js
================================================
import express from "express";
import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/audit-log
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log
*
* Retrieve all logs
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific audit log entry
*
* /api/audit-log/123
*/
router
.route("/:event_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log/123
*
* Retrieve a specific entry
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["event_id"],
additionalProperties: false,
properties: {
event_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
event_id: req.params.event_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const item = await internalAuditLog.get(res.locals.access, {
id: data.event_id,
expand: data.expand,
});
res.status(200).send(item);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/main.js
================================================
import express from "express";
import errs from "../lib/error.js";
import pjson from "../package.json" with { type: "json" };
import { isSetup } from "../setup.js";
import auditLogRoutes from "./audit-log.js";
import accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js";
import deadHostsRoutes from "./nginx/dead_hosts.js";
import proxyHostsRoutes from "./nginx/proxy_hosts.js";
import redirectionHostsRoutes from "./nginx/redirection_hosts.js";
import streamsRoutes from "./nginx/streams.js";
import reportsRoutes from "./reports.js";
import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js";
import versionRoutes from "./version.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* Health Check
* GET /api
*/
router.get("/", async (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
res.status(200).send({
status: "OK",
setup,
version: {
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
});
router.use("/schema", schemaRoutes);
router.use("/tokens", tokensRoutes);
router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes);
router.use("/version", versionRoutes);
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes);
router.use("/nginx/streams", streamsRoutes);
router.use("/nginx/access-lists", accessListsRoutes);
router.use("/nginx/certificates", certificatesHostsRoutes);
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, (req, _, next) => {
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
});
export default router;
================================================
FILE: backend/routes/nginx/access_lists.js
================================================
import express from "express";
import internalAccessList from "../../internal/access-list.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/access-lists
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists
*
* Retrieve all access-lists
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/access-lists
*
* Create a new access-list
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body);
const result = await internalAccessList.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific access-list
*
* /api/nginx/access-lists/123
*/
router
.route("/:list_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists/123
*
* Retrieve a specific access-list
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["list_id"],
additionalProperties: false,
properties: {
list_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
list_id: req.params.list_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalAccessList.get(res.locals.access, {
id: Number.parseInt(data.list_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/access-lists/123
*
* Update and existing access-list
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body);
payload.id = Number.parseInt(req.params.list_id, 10);
const result = await internalAccessList.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/access-lists/123
*
* Delete and existing access-list
*/
.delete(async (req, res, next) => {
try {
const result = await internalAccessList.delete(res.locals.access, {
id: Number.parseInt(req.params.list_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/nginx/certificates.js
================================================
import express from "express";
import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" };
import internalCertificate from "../../internal/certificate.js";
import errs from "../../lib/error.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/certificates
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates
*
* Retrieve all certificates
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalCertificate.getAll(
res.locals.access,
data.expand,
data.query,
);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/certificates
*
* Create a new certificate
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/certificates", "post"),
req.body,
);
req.setTimeout(900000); // 15 minutes timeout
const result = await internalCertificate.create(
res.locals.access,
payload,
);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* /api/nginx/certificates/dns-providers
*/
router
.route("/dns-providers")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/dns-providers
*
* Get list of all supported DNS providers
*/
.get(async (req, res, next) => {
try {
if (!res.locals.access.token.getUserId()) {
throw new errs.PermissionError("Login required");
}
const clean = Object.keys(dnsPlugins).map((key) => ({
id: key,
name: dnsPlugins[key].name,
credentials: dnsPlugins[key].credentials,
}));
clean.sort((a, b) => a.name.localeCompare(b.name));
res.status(200).send(clean);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Test HTTP challenge for domains
*
* /api/nginx/certificates/test-http
*/
router
.route("/test-http")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/certificates/test-http", "post"),
req.body,
);
req.setTimeout(60000); // 1 minute timeout
const result = await internalCertificate.testHttpsChallenge(
res.locals.access,
payload,
);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Validate Certs before saving
*
* /api/nginx/certificates/validate
*/
router
.route("/validate")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/validate
*
* Validate certificates
*/
.post(async (req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.validate({
files: req.files,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route("/:certificate_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["certificate_id"],
additionalProperties: false,
properties: {
certificate_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
certificate_id: req.params.certificate_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const row = await internalCertificate.get(res.locals.access, {
id: Number.parseInt(data.certificate_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete(async (req, res, next) => {
try {
const result = await internalCertificate.delete(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route("/:certificate_id/upload")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post(async (req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.upload(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
files: req.files,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route("/:certificate_id/renew")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post(async (req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
try {
const result = await internalCertificate.renew(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route("/:certificate_id/download")
.options((_req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get(async (req, res, next) => {
try {
const result = await internalCertificate.download(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).download(result.fileName);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/nginx/dead_hosts.js
================================================
import express from "express";
import internalDeadHost from "../../internal/dead-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/dead-hosts
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts
*
* Retrieve all dead-hosts
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalDeadHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/dead-hosts
*
* Create a new dead-host
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body);
const result = await internalDeadHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific dead-host
*
* /api/nginx/dead-hosts/123
*/
router
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts/123
*
* Retrieve a specific dead-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalDeadHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/dead-hosts/123
*
* Update an existing dead-host
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalDeadHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/dead-hosts/123
*
* Delete a dead-host
*/
.delete(async (req, res, next) => {
try {
const result = await internalDeadHost.delete(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable dead-host
*
* /api/nginx/dead-hosts/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalDeadHost.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable dead-host
*
* /api/nginx/dead-hosts/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
try {
const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) });
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/nginx/proxy_hosts.js
================================================
import express from "express";
import internalProxyHost from "../../internal/proxy-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/proxy-hosts
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts
*
* Retrieve all proxy-hosts
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalProxyHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/proxy-hosts
*
* Create a new proxy-host
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body);
const result = await internalProxyHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err} ${JSON.stringify(err.debug, null, 2)}`);
next(err);
}
});
/**
* Specific proxy-host
*
* /api/nginx/proxy-hosts/123
*/
router
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts/123
*
* Retrieve a specific proxy-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalProxyHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalProxyHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.delete(async (req, res, next) => {
try {
const result = await internalProxyHost.delete(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable proxy-host
*
* /api/nginx/proxy-hosts/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable proxy-host
*
* /api/nginx/proxy-hosts/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/disable
*/
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.disable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/nginx/redirection_hosts.js
================================================
import express from "express";
import internalRedirectionHost from "../../internal/redirection-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/redirection-hosts
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts
*
* Retrieve all redirection-hosts
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/redirection-hosts
*
* Create a new redirection-host
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body);
const result = await internalRedirectionHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific redirection-host
*
* /api/nginx/redirection-hosts/123
*/
router
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts/123
*
* Retrieve a specific redirection-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalRedirectionHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"),
req.body,
);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalRedirectionHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.delete(async (req, res, next) => {
try {
const result = await internalRedirectionHost.delete(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable redirection-host
*
* /api/nginx/redirection-hosts/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalRedirectionHost.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable redirection-host
*
* /api/nginx/redirection-hosts/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/disable
*/
.post(async (req, res, next) => {
try {
const result = await internalRedirectionHost.disable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/nginx/streams.js
================================================
import express from "express";
import internalStream from "../../internal/stream.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/streams
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams
*
* Retrieve all streams
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalStream.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/streams
*
* Create a new stream
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/streams", "post"), req.body);
const result = await internalStream.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific stream
*
* /api/nginx/streams/123
*/
router
.route("/:stream_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams/123
*
* Retrieve a specific stream
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["stream_id"],
additionalProperties: false,
properties: {
stream_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
stream_id: req.params.stream_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalStream.get(res.locals.access, {
id: Number.parseInt(data.stream_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/streams/123
*
* Update and existing stream
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body);
payload.id = Number.parseInt(req.params.stream_id, 10);
const result = await internalStream.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/streams/123
*
* Update and existing stream
*/
.delete(async (req, res, next) => {
try {
const result = await internalStream.delete(res.locals.access, {
id: Number.parseInt(req.params.stream_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable stream
*
* /api/nginx/streams/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalStream.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable stream
*
* /api/nginx/streams/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/disable
*/
.post(async (req, res, next) => {
try {
const result = await internalStream.disable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/reports.js
================================================
import express from "express";
import internalReport from "../internal/report.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
router
.route("/hosts")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /reports/hosts
*/
.get(async (req, res, next) => {
try {
const data = await internalReport.getHostsReport(res.locals.access);
res.status(200).send(data);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/schema.js
================================================
import express from "express";
import { debug, express as logger } from "../logger.js";
import PACKAGE from "../package.json" with { type: "json" };
import { getCompiledSchema } from "../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
/**
* GET /schema
*/
.get(async (req, res) => {
try {
const swaggerJSON = await getCompiledSchema();
let proto = req.protocol;
if (typeof req.headers["x-forwarded-proto"] !== "undefined" && req.headers["x-forwarded-proto"]) {
proto = req.headers["x-forwarded-proto"];
}
let origin = `${proto}://${req.hostname}`;
if (typeof req.headers.origin !== "undefined" && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = `${origin}/api`;
res.status(200).send(swaggerJSON);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/settings.js
================================================
import express from "express";
import internalSetting from "../internal/setting.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/settings
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/settings
*
* Retrieve all settings
*/
.get(async (req, res, next) => {
try {
const rows = await internalSetting.getAll(res.locals.access);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific setting
*
* /api/settings/something
*/
router
.route("/:setting_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /settings/something
*
* Retrieve a specific setting
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["setting_id"],
additionalProperties: false,
properties: {
setting_id: {
type: "string",
minLength: 1,
},
},
},
{
setting_id: req.params.setting_id,
},
);
const row = await internalSetting.get(res.locals.access, {
id: data.setting_id,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/settings/something
*
* Update and existing setting
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body);
payload.id = req.params.setting_id;
const result = await internalSetting.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/tokens.js
================================================
import express from "express";
import internalToken from "../internal/token.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
/**
* GET /tokens
*
* Get a new Token, given they already have a token they want to refresh
* We also piggy back on to this method, allowing admins to get tokens
* for services like Job board and Worker.
*/
.get(jwtdecode(), async (req, res, next) => {
try {
const data = await internalToken.getFreshToken(res.locals.access, {
expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null,
scope: typeof req.query.scope !== "undefined" ? req.query.scope : null,
});
res.status(200).send(data);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /tokens
*
* Create a new Token
*/
.post(async (req, res, next) => {
try {
const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body);
const result = await internalToken.getTokenFromEmail(data);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
router
.route("/2fa")
.options((_, res) => {
res.sendStatus(204);
})
/**
* POST /tokens/2fa
*
* Verify 2FA code and get full token
*/
.post(async (req, res, next) => {
try {
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const result = await internalToken.verify2FA(challenge_token, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/users.js
================================================
import express from "express";
import internal2FA from "../internal/2fa.js";
import internalUser from "../internal/user.js";
import Access from "../lib/access.js";
import { isCI } from "../lib/config.js";
import errs from "../lib/error.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import userIdFromMe from "../lib/express/user-id-from-me.js";
import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
import { isSetup } from "../setup.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/users
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/users
*
* Retrieve all users
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const users = await internalUser.getAll(
res.locals.access,
data.expand,
data.query,
);
res.status(200).send(users);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/users
*
* Create a new User
*/
.post(async (req, res, next) => {
const body = req.body;
try {
// If we are in setup mode, we don't check access for current user
const setup = await isSetup();
if (!setup) {
logger.info("Creating a new user in setup mode");
const access = new Access(null);
await access.load(true);
res.locals.access = access;
// We are in setup mode, set some defaults for this first new user, such as making
// them an admin.
body.is_disabled = false;
if (typeof body.roles !== "object" || body.roles === null) {
body.roles = [];
}
if (body.roles.indexOf("admin") === -1) {
body.roles.push("admin");
}
}
const payload = await apiValidator(
getValidationSchema("/users", "post"),
body,
);
const user = await internalUser.create(res.locals.access, payload);
res.status(201).send(user);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users
*
* Deletes ALL users. This is NOT GENERALLY AVAILABLE!
* (!) It is NOT an authenticated endpoint.
* (!) Only CI should be able to call this endpoint. As a result,
*
* it will only work when the env vars DEBUG=true and CI=true
*
* Do NOT set those env vars in a production environment!
*/
.delete(async (_, res, next) => {
if (isCI()) {
try {
logger.warn("Deleting all users - CI environment detected, allowing this operation");
await internalUser.deleteAll();
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
return;
}
next(new errs.ItemNotFoundError());
});
/**
* Specific user
*
* /api/users/123
*/
router
.route("/:user_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* GET /users/123 or /users/me
*
* Retrieve a specific user
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["user_id"],
additionalProperties: false,
properties: {
user_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
user_id: req.params.user_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const user = await internalUser.get(res.locals.access, {
id: data.user_id,
expand: data.expand,
omit: internalUser.getUserOmisionsByAccess(
res.locals.access,
data.user_id,
),
});
res.status(200).send(user);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/users/123
*
* Update and existing user
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}", "put"),
req.body,
);
payload.id = req.params.user_id;
const result = await internalUser.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users/123
*
* Update and existing user
*/
.delete(async (req, res, next) => {
try {
const result = await internalUser.delete(res.locals.access, {
id: req.params.user_id,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific user auth
*
* /api/users/123/auth
*/
router
.route("/:user_id/auth")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/auth
*
* Update password for a user
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}/auth", "put"),
req.body,
);
payload.id = req.params.user_id;
const result = await internalUser.setPassword(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific user permissions
*
* /api/users/123/permissions
*/
router
.route("/:user_id/permissions")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/permissions
*
* Set some or all permissions for a user
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}/permissions", "put"),
req.body,
);
payload.id = req.params.user_id;
const result = await internalUser.setPermissions(
res.locals.access,
payload,
);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific user login as
*
* /api/users/123/login
*/
router
.route("/:user_id/login")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/users/123/login
*
* Log in as a user
*/
.post(async (req, res, next) => {
try {
const result = await internalUser.loginAs(res.locals.access, {
id: Number.parseInt(req.params.user_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA status
*
* /api/users/123/2fa
*/
router
.route("/:user_id/2fa")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const result = await internal2FA.startSetup(res.locals.access, req.params.user_id);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* GET /api/users/123/2fa
*
* Get 2FA status for a user
*/
.get(async (req, res, next) => {
try {
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
res.status(200).send(status);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users/123/2fa?code=XXXXXX
*
* Disable 2FA for a user
*/
.delete(async (req, res, next) => {
try {
const code = typeof req.query.code === "string" ? req.query.code : null;
if (!code) {
throw new errs.ValidationError("Missing required parameter: code");
}
await internal2FA.disable(res.locals.access, req.params.user_id, code);
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA enable
*
* /api/users/123/2fa/enable
*/
router
.route("/:user_id/2fa/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/enable
*
* Verify code and enable 2FA
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/enable", "post"),
req.body,
);
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA backup codes
*
* /api/users/123/2fa/backup-codes
*/
router
.route("/:user_id/2fa/backup-codes")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/backup-codes
*
* Regenerate backup codes
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/backup-codes", "post"),
req.body,
);
const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
================================================
FILE: backend/routes/version.js
================================================
import express from "express";
import internalRemoteVersion from "../internal/remote-version.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/version/check
*/
router
.route("/check")
.options((_, res) => {
res.sendStatus(204);
})
/**
* GET /api/version/check
*
* Check for available updates
*/
.get(async (req, res, _next) => {
try {
const data = await internalRemoteVersion.get();
res.status(200).send(data);
} catch (error) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
// Send 200 even though there's an error to avoid triggering update checks repeatedly
res.status(200).send({
current: null,
latest: null,
update_available: false,
});
}
});
export default router;
================================================
FILE: backend/schema/common.json
================================================
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "common",
"type": "object",
"properties": {
"id": {
"description": "Unique identifier",
"readOnly": true,
"type": "integer",
"minimum": 1,
"example": 11
},
"expand": {
"anyOf": [
{
"type": "null"
},
{
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
]
},
"query": {
"anyOf": [
{
"type": "null"
},
{
"type": "string",
"minLength": 1,
"maxLength": 255
}
]
},
"created_on": {
"description": "Date and time of creation",
"readOnly": true,
"type": "string",
"example": "2025-10-28T04:17:54.000Z"
},
"modified_on": {
"description": "Date and time of last update",
"readOnly": true,
"type": "string",
"example": "2025-10-28T04:17:54.000Z"
},
"user_id": {
"description": "User ID",
"type": "integer",
"minimum": 1,
"example": 2
},
"certificate_id": {
"description": "Certificate ID",
"anyOf": [
{
"type": "integer",
"minimum": 0,
"example": 5
},
{
"type": "string",
"pattern": "^new$",
"example": "new"
}
],
"example": 5
},
"access_list_id": {
"description": "Access List ID",
"type": "integer",
"minimum": 0,
"example": 3
},
"domain_names": {
"description": "Domain Names separated by a comma",
"type": "array",
"minItems": 1,
"maxItems": 100,
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
},
"example": ["example.com", "www.example.com"]
},
"enabled": {
"description": "Is Enabled",
"type": "boolean",
"example": false
},
"ssl_forced": {
"description": "Is SSL Forced",
"type": "boolean",
"example": true
},
"hsts_enabled": {
"description": "Is HSTS Enabled",
"type": "boolean",
"example": true
},
"hsts_subdomains": {
"description": "Is HSTS applicable to all subdomains",
"type": "boolean",
"example": true
},
"ssl_provider": {
"type": "string",
"pattern": "^(letsencrypt|other)$",
"example": "letsencrypt"
},
"http2_support": {
"description": "HTTP2 Protocol Support",
"type": "boolean",
"example": true
},
"block_exploits": {
"description": "Should we block common exploits",
"type": "boolean",
"example": false
},
"caching_enabled": {
"description": "Should we cache assets",
"type": "boolean",
"example": true
},
"email": {
"description": "Email address",
"type": "string",
"pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
"example": "me@example.com"
},
"directive": {
"type": "string",
"enum": ["allow", "deny"],
"example": "allow"
},
"address": {
"oneOf": [
{
"type": "string",
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
},
{
"type": "string",
"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
},
{
"type": "string",
"pattern": "^all$"
}
],
"example": "192.168.0.11"
},
"access_items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string"
}
},
"example": {
"username": "admin",
"password": "pass"
}
},
"example": [
{
"username": "admin",
"password": "pass"
}
]
},
"access_clients": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"address": {
"$ref": "#/properties/address"
},
"directive": {
"$ref": "#/properties/directive"
}
},
"example": {
"directive": "allow",
"address": "192.168.0.0/24"
}
},
"example": [
{
"directive": "allow",
"address": "192.168.0.0/24"
}
]
},
"certificate_files": {
"description": "Certificate Files",
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["certificate", "certificate_key"],
"properties": {
"certificate": {
"type": "string",
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
},
"certificate_key": {
"type": "string",
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
},
"intermediate_certificate": {
"type": "string",
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
}
}
},
"example": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----",
"certificate_key": "-----BEGIN PRIVATE\nMIID...-----END CERTIFICATE-----"
}
}
}
}
}
}
================================================
FILE: backend/schema/components/access-list-object.json
================================================
{
"type": "object",
"description": "Access List object",
"required": ["id", "created_on", "modified_on", "owner_user_id", "name", "meta", "satisfy_any", "pass_auth", "proxy_host_count"],
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"name": {
"type": "string",
"minLength": 1,
"example": "My Access List"
},
"meta": {
"type": "object",
"example": {}
},
"satisfy_any": {
"type": "boolean",
"example": true
},
"pass_auth": {
"type": "boolean",
"example": false
},
"proxy_host_count": {
"type": "integer",
"minimum": 0,
"example": 3
}
}
}
================================================
FILE: backend/schema/components/audit-log-list.json
================================================
{
"type": "array",
"description": "Audit Log list",
"items": {
"$ref": "./audit-log-object.json"
}
}
================================================
FILE: backend/schema/components/audit-log-object.json
================================================
{
"type": "object",
"description": "Audit Log object",
"required": [
"id",
"created_on",
"modified_on",
"user_id",
"object_type",
"object_id",
"action",
"meta"
],
"additionalProperties": false,
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"user_id": {
"$ref": "../common.json#/properties/user_id"
},
"object_type": {
"type": "string",
"example": "certificate"
},
"object_id": {
"$ref": "../common.json#/properties/id"
},
"action": {
"type": "string",
"example": "created"
},
"meta": {
"type": "object",
"example": {}
},
"user": {
"$ref": "./user-object.json"
}
}
}
================================================
FILE: backend/schema/components/certificate-list.json
================================================
{
"type": "array",
"description": "Certificates list",
"items": {
"$ref": "./certificate-object.json"
}
}
================================================
FILE: backend/schema/components/certificate-object.json
================================================
{
"type": "object",
"description": "Certificate object",
"required": ["id", "created_on", "modified_on", "owner_user_id", "provider", "nice_name", "domain_names", "expires_on", "meta"],
"additionalProperties": false,
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"provider": {
"$ref": "../common.json#/properties/ssl_provider"
},
"nice_name": {
"type": "string",
"description": "Nice Name for the custom certificate",
"example": "My Custom Cert"
},
"domain_names": {
"description": "Domain Names separated by a comma",
"type": "array",
"maxItems": 100,
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
},
"example": ["example.com", "www.example.com"]
},
"expires_on": {
"description": "Date and time of expiration",
"readOnly": true,
"type": "string",
"example": "2025-10-28T04:17:54.000Z"
},
"owner": {
"$ref": "./user-object.json"
},
"meta": {
"type": "object",
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string",
"minLength": 1
},
"certificate_key": {
"type": "string",
"minLength": 1
},
"dns_challenge": {
"type": "boolean"
},
"dns_provider_credentials": {
"type": "string"
},
"dns_provider": {
"type": "string"
},
"letsencrypt_certificate": {
"type": "object"
},
"propagation_seconds": {
"type": "integer",
"minimum": 0
},
"key_type": {
"type": "string",
"enum": ["rsa", "ecdsa"],
"default": "rsa"
}
},
"example": {
"dns_challenge": false
}
}
}
}
================================================
FILE: backend/schema/components/check-version-object.json
================================================
{
"type": "object",
"description": "Check Version object",
"additionalProperties": false,
"required": ["current", "latest", "update_available"],
"properties": {
"current": {
"type": ["string", "null"],
"description": "Current version string",
"example": "v2.10.1"
},
"latest": {
"type": ["string", "null"],
"description": "Latest version string",
"example": "v2.13.4"
},
"update_available": {
"type": "boolean",
"description": "Whether there's an update available",
"example": true
}
}
}
================================================
FILE: backend/schema/components/dead-host-list.json
================================================
{
"type": "array",
"description": "404 Hosts list",
"items": {
"$ref": "./dead-host-object.json"
}
}
================================================
FILE: backend/schema/components/dead-host-object.json
================================================
{
"type": "object",
"description": "404 Host object",
"required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "advanced_config", "enabled", "meta"],
"additionalProperties": false,
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"domain_names": {
"$ref": "../common.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../common.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../common.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../common.json#/properties/http2_support"
},
"advanced_config": {
"type": "string",
"example": ""
},
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"meta": {
"type": "object",
"example": {}
},
"certificate": {
"oneOf": [
{
"type": "null",
"example": null
},
{
"$ref": "./certificate-object.json"
}
],
"example": null
},
"owner": {
"$ref": "./user-object.json"
}
}
}
================================================
FILE: backend/schema/components/dns-providers-list.json
================================================
{
"type": "array",
"description": "DNS Providers list",
"items": {
"type": "object",
"required": ["id", "name", "credentials"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the DNS provider, matching the python package"
},
"name": {
"type": "string",
"description": "Human-readable name of the DNS provider"
},
"credentials": {
"type": "string",
"description": "Instructions on how to format the credentials for this DNS provider"
}
}
}
}
================================================
FILE: backend/schema/components/error-object.json
================================================
{
"type": "object",
"description": "Error object",
"additionalProperties": false,
"required": ["code", "message"],
"properties": {
"code": {
"type": "integer",
"example": 400
},
"message": {
"type": "string",
"example": "Bad Request"
}
}
}
================================================
FILE: backend/schema/components/error.json
================================================
{
"type": "object",
"description": "Error",
"properties": {
"error": {
"$ref": "./error-object.json"
}
}
}
================================================
FILE: backend/schema/components/health-object.json
================================================
{
"type": "object",
"description": "Health object",
"additionalProperties": false,
"required": ["status", "version"],
"properties": {
"status": {
"type": "string",
"description": "Healthy",
"example": "OK"
},
"setup": {
"type": "boolean",
"description": "Whether the initial setup has been completed",
"example": true
},
"version": {
"type": "object",
"description": "The version object",
"example": {
"major": 2,
"minor": 0,
"revision": 0
},
"additionalProperties": false,
"required": ["major", "minor", "revision"],
"properties": {
"major": {
"type": "integer",
"minimum": 0,
"example": 2
},
"minor": {
"type": "integer",
"minimum": 0,
"example": 10
},
"revision": {
"type": "integer",
"minimum": 0,
"example": 1
}
}
}
}
}
================================================
FILE: backend/schema/components/permission-object.json
================================================
{
"type": "object",
"minProperties": 1,
"properties": {
"visibility": {
"type": "string",
"description": "Visibility Type",
"enum": ["all", "user"],
"example": "all"
},
"access_lists": {
"type": "string",
"description": "Access Lists Permissions",
"enum": ["hidden", "view", "manage"],
"example": "view"
},
"dead_hosts": {
"type": "string",
"description": "404 Hosts Permissions",
"enum": ["hidden", "view", "manage"],
"example": "manage"
},
"proxy_hosts": {
"type": "string",
"description": "Proxy Hosts Permissions",
"enum": ["hidden", "view", "manage"],
"example": "hidden"
},
"redirection_hosts": {
"type": "string",
"description": "Redirection Permissions",
"enum": ["hidden", "view", "manage"],
"example": "view"
},
"streams": {
"type": "string",
"description": "Streams Permissions",
"enum": ["hidden", "view", "manage"],
"example": "manage"
},
"certificates": {
"type": "string",
"description": "Certificates Permissions",
"enum": ["hidden", "view", "manage"],
"example": "hidden"
}
}
}
================================================
FILE: backend/schema/components/proxy-host-list.json
================================================
{
"type": "array",
"description": "Proxy Hosts list",
"items": {
"$ref": "./proxy-host-object.json"
}
}
================================================
FILE: backend/schema/components/proxy-host-object.json
================================================
{
"type": "object",
"description": "Proxy Host object",
"required": [
"id",
"created_on",
"modified_on",
"owner_user_id",
"domain_names",
"forward_host",
"forward_port",
"access_list_id",
"certificate_id",
"ssl_forced",
"caching_enabled",
"block_exploits",
"advanced_config",
"meta",
"allow_websocket_upgrade",
"http2_support",
"forward_scheme",
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto"
],
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"domain_names": {
"$ref": "../common.json#/properties/domain_names"
},
"forward_host": {
"type": "string",
"minLength": 1,
"maxLength": 255,
"example": "127.0.0.1"
},
"forward_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"example": 8080
},
"access_list_id": {
"$ref": "../common.json#/properties/access_list_id"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../common.json#/properties/ssl_forced"
},
"caching_enabled": {
"$ref": "../common.json#/properties/caching_enabled"
},
"block_exploits": {
"$ref": "../common.json#/properties/block_exploits"
},
"advanced_config": {
"type": "string",
"example": ""
},
"meta": {
"type": "object",
"example": {
"nginx_online": true,
"nginx_err": null
}
},
"allow_websocket_upgrade": {
"description": "Allow Websocket Upgrade for all paths",
"type": "boolean",
"example": true
},
"http2_support": {
"$ref": "../common.json#/properties/http2_support"
},
"forward_scheme": {
"type": "string",
"enum": ["http", "https"],
"example": "http"
},
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"locations": {
"type": "array",
"minItems": 0,
"items": {
"type": "object",
"required": ["forward_scheme", "forward_host", "forward_port", "path"],
"additionalProperties": false,
"properties": {
"id": {
"type": ["integer", "null"]
},
"path": {
"type": "string",
"minLength": 1
},
"forward_scheme": {
"$ref": "#/properties/forward_scheme"
},
"forward_host": {
"$ref": "#/properties/forward_host"
},
"forward_port": {
"$ref": "#/properties/forward_port"
},
"forward_path": {
"type": "string"
},
"advanced_config": {
"type": "string"
}
}
},
"example": [
{
"path": "/app",
"forward_scheme": "http",
"forward_host": "example.com",
"forward_port": 80
}
]
},
"hsts_enabled": {
"$ref": "../common.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto":{
"type": "boolean",
"description": "Trust the forwarded headers",
"example": false
},
"certificate": {
"oneOf": [
{
"type": "null",
"example": null
},
{
"$ref": "./certificate-object.json"
}
],
"example": null
},
"owner": {
"$ref": "./user-object.json"
},
"access_list": {
"oneOf": [
{
"type": "null",
"example": null
},
{
"$ref": "./access-list-object.json"
}
],
"example": null
}
}
}
================================================
FILE: backend/schema/components/redirection-host-list.json
================================================
{
"type": "array",
"description": "Redirection Hosts list",
"items": {
"$ref": "./redirection-host-object.json"
}
}
================================================
FILE: backend/schema/components/redirection-host-object.json
================================================
{
"type": "object",
"description": "Redirection Host object",
"required": [
"id",
"created_on",
"modified_on",
"owner_user_id",
"domain_names",
"forward_http_code",
"forward_scheme",
"forward_domain_name",
"preserve_path",
"certificate_id",
"ssl_forced",
"hsts_enabled",
"hsts_subdomains",
"http2_support",
"block_exploits",
"advanced_config",
"enabled",
"meta"
],
"additionalProperties": false,
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"domain_names": {
"$ref": "../common.json#/properties/domain_names"
},
"forward_http_code": {
"description": "Redirect HTTP Status Code",
"type": "integer",
"minimum": 300,
"maximum": 308,
"example": 302
},
"forward_scheme": {
"type": "string",
"enum": [
"auto",
"http",
"https"
],
"example": "http"
},
"forward_domain_name": {
"description": "Domain Name",
"type": "string",
"pattern": "^(?:[^.*]+\\.?)+[^.]$",
"example": "jc21.com"
},
"preserve_path": {
"description": "Should the path be preserved",
"type": "boolean",
"example": true
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../common.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../common.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../common.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../common.json#/properties/block_exploits"
},
"advanced_config": {
"type": "string",
"example": ""
},
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"meta": {
"type": "object",
"example": {
"nginx_online": true,
"nginx_err": null
}
},
"certificate": {
"oneOf": [
{
"type": "null",
"example": null
},
{
"$ref": "./certificate-object.json"
}
],
"example": null
},
"owner": {
"$ref": "./user-object.json"
}
}
}
================================================
FILE: backend/schema/components/security-schemes.json
================================================
{
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer Token authentication"
}
}
================================================
FILE: backend/schema/components/setting-list.json
================================================
{
"type": "array",
"description": "Setting list",
"items": {
"$ref": "./setting-object.json"
}
}
================================================
FILE: backend/schema/components/setting-object.json
================================================
{
"type": "object",
"description": "Setting object",
"required": ["id", "name", "description", "value", "meta"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Setting ID",
"minLength": 1,
"example": "default-site"
},
"name": {
"type": "string",
"description": "Setting Display Name",
"minLength": 1,
"example": "Default Site"
},
"description": {
"type": "string",
"description": "Meaningful description",
"minLength": 1,
"example": "What to show when Nginx is hit with an unknown Host"
},
"value": {
"description": "Value in almost any form",
"example": "congratulations",
"anyOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "integer"
},
{
"type": "object"
},
{
"type": "number"
},
{
"type": "array"
}
]
},
"meta": {
"description": "Extra metadata",
"example": {
"redirect": "http://example.com",
"html": "404 "
},
"type": "object"
}
}
}
================================================
FILE: backend/schema/components/stream-list.json
================================================
{
"type": "array",
"description": "Streams list",
"items": {
"$ref": "./stream-object.json"
}
}
================================================
FILE: backend/schema/components/stream-object.json
================================================
{
"type": "object",
"description": "Stream object",
"required": [
"id",
"created_on",
"modified_on",
"owner_user_id",
"incoming_port",
"forwarding_host",
"forwarding_port",
"tcp_forwarding",
"udp_forwarding",
"enabled",
"meta"
],
"additionalProperties": false,
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"created_on": {
"$ref": "../common.json#/properties/created_on"
},
"modified_on": {
"$ref": "../common.json#/properties/modified_on"
},
"owner_user_id": {
"$ref": "../common.json#/properties/user_id"
},
"incoming_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"example": 9090
},
"forwarding_host": {
"anyOf": [
{
"description": "Domain Name",
"type": "string",
"pattern": "^(?:[^.*]+\\.?)+[^.]$",
"example": "example.com"
},
{
"type": "string",
"format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
},
{
"type": "string",
"format": "ipv6"
}
],
"example": "example.com"
},
"forwarding_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"example": 80
},
"tcp_forwarding": {
"type": "boolean",
"example": true
},
"udp_forwarding": {
"type": "boolean",
"example": false
},
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"meta": {
"type": "object",
"example": {}
},
"certificate": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./certificate-object.json"
}
],
"example": null
},
"owner": {
"$ref": "./user-object.json"
}
}
}
================================================
FILE: backend/schema/components/token-challenge.json
================================================
{
"type": "object",
"description": "Token object",
"required": ["requires_2fa", "challenge_token"],
"additionalProperties": false,
"properties": {
"requires_2fa": {
"description": "Whether this token request requires two-factor authentication",
"example": true,
"type": "boolean"
},
"challenge_token": {
"description": "Challenge Token used in subsequent 2FA verification",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"type": "string"
}
}
}
================================================
FILE: backend/schema/components/token-object.json
================================================
{
"type": "object",
"description": "Token object",
"required": ["expires", "token"],
"additionalProperties": false,
"properties": {
"expires": {
"description": "Token Expiry ISO Time String",
"example": "2025-02-04T20:40:46.340Z",
"type": "string"
},
"token": {
"description": "JWT Token",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"type": "string"
}
}
}
================================================
FILE: backend/schema/components/user-list.json
================================================
{
"type": "array",
"description": "User list",
"items": {
"$ref": "./user-object.json"
}
}
================================================
FILE: backend/schema/components/user-object.json
================================================
{
"type": "object",
"description": "User object",
"required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"description": "User ID",
"minimum": 1,
"example": 1
},
"created_on": {
"type": "string",
"description": "Created Date",
"example": "2020-01-30T09:36:08.000Z"
},
"modified_on": {
"type": "string",
"description": "Modified Date",
"example": "2020-01-30T09:41:04.000Z"
},
"is_disabled": {
"type": "boolean",
"description": "Is user Disabled",
"example": true
},
"email": {
"type": "string",
"description": "Email",
"minLength": 3,
"example": "jc@jc21.com"
},
"name": {
"type": "string",
"description": "Name",
"minLength": 1,
"example": "Jamie Curnow"
},
"nickname": {
"type": "string",
"description": "Nickname",
"example": "James"
},
"avatar": {
"type": "string",
"description": "Gravatar URL based on email, without scheme",
"example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm"
},
"roles": {
"description": "Roles applied",
"example": ["admin"],
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"type": "object",
"description": "Permissions if expanded in request",
"required": [
"visibility",
"proxy_hosts",
"redirection_hosts",
"dead_hosts",
"streams",
"access_lists",
"certificates"
],
"properties": {
"visibility": {
"type": "string",
"description": "Visibility level",
"example": "all",
"pattern": "^(all|user)$"
},
"proxy_hosts": {
"type": "string",
"description": "Proxy Hosts access level",
"example": "manage",
"pattern": "^(manage|view|hidden)$"
},
"redirection_hosts": {
"type": "string",
"description": "Redirection Hosts access level",
"example": "manage",
"pattern": "^(manage|view|hidden)$"
},
"dead_hosts": {
"type": "string",
"description": "Dead Hosts access level",
"example": "manage",
"pattern": "^(manage|view|hidden)$"
},
"streams": {
"type": "string",
"description": "Streams access level",
"example": "manage",
"pattern": "^(manage|view|hidden)$"
},
"access_lists": {
"type": "string",
"description": "Access Lists access level",
"example": "hidden",
"pattern": "^(manage|view|hidden)$"
},
"certificates": {
"type": "string",
"description": "Certificates access level",
"example": "view",
"pattern": "^(manage|view|hidden)$"
}
}
}
}
}
================================================
FILE: backend/schema/index.js
================================================
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import $RefParser from "@apidevtools/json-schema-ref-parser";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
let compiledSchema = null;
/**
* Compiles the schema, by dereferencing it, only once
* and returns the memory cached value
*/
const getCompiledSchema = async () => {
if (compiledSchema === null) {
compiledSchema = await $RefParser.dereference(`${__dirname}/swagger.json`, {
mutateInputSchema: false,
});
}
return compiledSchema;
};
/**
* Scans the schema for the validation schema for the given path and method
* and returns it.
*
* @param {string} path
* @param {string} method
* @returns string|null
*/
const getValidationSchema = (path, method) => {
if (
compiledSchema !== null &&
typeof compiledSchema.paths[path] !== "undefined" &&
typeof compiledSchema.paths[path][method] !== "undefined" &&
typeof compiledSchema.paths[path][method].requestBody !== "undefined" &&
typeof compiledSchema.paths[path][method].requestBody.content !== "undefined" &&
typeof compiledSchema.paths[path][method].requestBody.content["application/json"] !== "undefined" &&
typeof compiledSchema.paths[path][method].requestBody.content["application/json"].schema !== "undefined"
) {
return compiledSchema.paths[path][method].requestBody.content["application/json"].schema;
}
return null;
};
export { getCompiledSchema, getValidationSchema };
================================================
FILE: backend/schema/paths/audit-log/get.json
================================================
{
"operationId": "getAuditLogs",
"summary": "Get Audit Logs",
"tags": ["audit-log"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 7,
"created_on": "2024-10-08T13:09:54.000Z",
"modified_on": "2024-10-08T13:09:54.000Z",
"user_id": 1,
"object_type": "user",
"object_id": 3,
"action": "updated",
"meta": {
"name": "John Doe",
"permissions": {
"user_id": 3,
"visibility": "all",
"access_lists": "manage",
"dead_hosts": "hidden",
"proxy_hosts": "manage",
"redirection_hosts": "view",
"streams": "hidden",
"certificates": "manage",
"id": 3,
"modified_on": "2024-10-08T13:09:54.000Z",
"created_on": "2024-10-08T13:09:51.000Z"
}
}
}
]
}
},
"schema": {
"$ref": "../../components/audit-log-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/audit-log/id/get.json
================================================
{
"operationId": "getAuditLog",
"summary": "Get Audit Log Event",
"tags": ["audit-log"],
"security": [
{
"bearerAuth": [
"admin"
]
}
],
"parameters": [
{
"in": "path",
"name": "id",
"description": "Audit Log Event ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2025-09-15T17:27:45.000Z",
"modified_on": "2025-09-15T17:27:45.000Z",
"user_id": 1,
"object_type": "user",
"object_id": 1,
"action": "created",
"meta": {
"id": 1,
"created_on": "2025-09-15T17:27:45.000Z",
"modified_on": "2025-09-15T17:27:45.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie",
"nickname": "Jamie",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
],
"permissions": {
"visibility": "all",
"proxy_hosts": "manage",
"redirection_hosts": "manage",
"dead_hosts": "manage",
"streams": "manage",
"access_lists": "manage",
"certificates": "manage"
}
}
}
}
},
"schema": {
"$ref": "../../../components/audit-log-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/get.json
================================================
{
"operationId": "health",
"summary": "Returns the API health status",
"tags": ["public"],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"status": "OK",
"setup": true,
"version": {
"major": 2,
"minor": 1,
"revision": 0
}
}
}
},
"schema": {
"$ref": "../components/health-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/access-lists/get.json
================================================
{
"operationId": "getAccessLists",
"summary": "Get all access lists",
"tags": ["access-lists"],
"security": [
{
"bearerAuth": [
"access_lists.view"
]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": [
"owner",
"items",
"clients",
"proxy_hosts"
]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"example": {
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"owner_user_id": 1,
"name": "test1234",
"meta": {},
"satisfy_any": true,
"pass_auth": false,
"proxy_host_count": 0
},
"schema": {
"$ref": "../../../components/access-list-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/access-lists/listID/delete.json
================================================
{
"operationId": "deleteAccessList",
"summary": "Delete a Access List",
"tags": ["access-lists"],
"security": [
{
"bearerAuth": ["access_lists.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "listID",
"description": "Access List ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/access-lists/listID/get.json
================================================
{
"operationId": "getAccessList",
"summary": "Get a access List",
"tags": [
"access-lists"
],
"security": [
{
"bearerAuth": [
"access_lists.view"
]
}
],
"parameters": [
{
"in": "path",
"name": "listID",
"description": "Access List ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2025-10-28T04:06:55.000Z",
"modified_on": "2025-10-29T22:48:20.000Z",
"owner_user_id": 1,
"name": "My Access List",
"meta": {},
"satisfy_any": false,
"pass_auth": false,
"proxy_host_count": 1
}
}
},
"schema": {
"$ref": "../../../../components/access-list-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/access-lists/listID/put.json
================================================
{
"operationId": "updateAccessList",
"summary": "Update a Access List",
"tags": ["access-lists"],
"security": [
{
"bearerAuth": ["access_lists.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "listID",
"description": "Access List ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"requestBody": {
"description": "Access List Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": {
"$ref": "../../../../components/access-list-object.json#/properties/name"
},
"satisfy_any": {
"$ref": "../../../../components/access-list-object.json#/properties/satisfy_any"
},
"pass_auth": {
"$ref": "../../../../components/access-list-object.json#/properties/pass_auth"
},
"items": {
"$ref": "../../../../common.json#/properties/access_items"
},
"clients": {
"$ref": "../../../../common.json#/properties/access_clients"
}
}
},
"example": {
"name": "My Access List",
"satisfy_any": true,
"pass_auth": false,
"items": [
{
"username": "admin2",
"password": "pass2"
}
],
"clients": [
{
"directive": "allow",
"address": "192.168.0.0/24"
}
]
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:34:34.000Z",
"owner_user_id": 1,
"name": "test123!!",
"meta": {},
"satisfy_any": true,
"pass_auth": false,
"proxy_host_count": 0,
"owner": {
"id": 1,
"created_on": "2024-10-07T22:43:55.000Z",
"modified_on": "2024-10-08T12:52:54.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "some guy",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": ["admin"]
},
"items": [
{
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"username": "admin",
"password": "",
"meta": {},
"hint": "a****"
},
{
"id": 2,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"username": "asdad",
"password": "",
"meta": {},
"hint": "a*****"
}
],
"clients": [
{
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"address": "127.0.0.1",
"directive": "allow",
"meta": {}
}
],
"proxy_hosts": []
}
}
},
"schema": {
"$ref": "../../../../components/access-list-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/access-lists/post.json
================================================
{
"operationId": "createAccessList",
"summary": "Create a Access List",
"tags": ["access-lists"],
"security": [
{
"bearerAuth": [
"access_lists.manage"
]
}
],
"requestBody": {
"description": "Access List Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"$ref": "../../../components/access-list-object.json#/properties/name"
},
"satisfy_any": {
"$ref": "../../../components/access-list-object.json#/properties/satisfy_any"
},
"pass_auth": {
"$ref": "../../../components/access-list-object.json#/properties/pass_auth"
},
"items": {
"$ref": "../../../common.json#/properties/access_items"
},
"clients": {
"$ref": "../../../common.json#/properties/access_clients"
}
}
},
"example": {
"name": "My Access List",
"satisfy_any": true,
"pass_auth": false,
"items": [
{
"username": "admin",
"password": "pass"
}
],
"clients": [
{
"directive": "allow",
"address": "192.168.0.0/24"
}
]
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"owner_user_id": 1,
"name": "test1234",
"meta": {},
"satisfy_any": true,
"pass_auth": false,
"proxy_host_count": 0,
"owner": {
"id": 1,
"created_on": "2024-10-07T22:43:55.000Z",
"modified_on": "2024-10-08T12:52:54.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "some guy",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": [
"admin"
]
},
"items": [
{
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"username": "admin",
"password": "",
"meta": {},
"hint": "a****"
},
{
"id": 2,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"username": "asdad",
"password": "",
"meta": {},
"hint": "a*****"
}
],
"proxy_hosts": [],
"clients": [
{
"id": 1,
"created_on": "2024-10-08T22:15:40.000Z",
"modified_on": "2024-10-08T22:15:40.000Z",
"access_list_id": 1,
"address": "127.0.0.1",
"directive": "allow",
"meta": {}
}
]
}
}
},
"schema": {
"$ref": "../../../components/access-list-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/certID/delete.json
================================================
{
"operationId": "deleteCertificate",
"summary": "Delete a Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "certID",
"description": "Certificate ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/certID/download/get.json
================================================
{
"operationId": "downloadCertificate",
"summary": "Downloads a Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "certID",
"description": "Certificate ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/zip": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/certID/get.json
================================================
{
"operationId": "getCertificate",
"summary": "Get a Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.view"]
}
],
"parameters": [
{
"in": "path",
"name": "certID",
"description": "Certificate ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 4,
"created_on": "2024-10-09T05:31:58.000Z",
"modified_on": "2024-10-09T05:32:11.000Z",
"owner_user_id": 1,
"provider": "letsencrypt",
"nice_name": "test.example.com",
"domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z",
"meta": {
"dns_challenge": false
}
}
}
},
"schema": {
"$ref": "../../../../components/certificate-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/certID/renew/post.json
================================================
{
"operationId": "renewCertificate",
"summary": "Renews a Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "certID",
"description": "Certificate ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires_on": "2025-01-07T06:41:58.000Z",
"modified_on": "2024-10-09T07:39:51.000Z",
"id": 4,
"created_on": "2024-10-09T05:31:58.000Z",
"owner_user_id": 1,
"provider": "letsencrypt",
"nice_name": "My Test Cert",
"domain_names": ["test.jc21.supernerd.pro"],
"meta": {
"dns_challenge": false
}
}
}
},
"schema": {
"$ref": "../../../../../components/certificate-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/certID/upload/post.json
================================================
{
"operationId": "uploadCertificate",
"summary": "Uploads a custom Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "certID",
"description": "Certificate ID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"requestBody": {
"$ref": "../../../../../common.json#/properties/certificate_files"
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw\ngZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1\ncm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD\nDDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu\nb3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe\nbWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93\nQEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu\nGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+\n2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU\nQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB\nIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE\nOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB\n/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy\nl8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s\nVXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn\nATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt\nIImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u\nm+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV\nXxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp\n1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw\nhp8bJUp/QN7pnOVCDbjTQ+HVMXw=\n-----END CERTIFICATE-----\n",
"certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n"
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["certificate", "certificate_key"],
"properties": {
"certificate": {
"type": "string",
"minLength": 1,
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
},
"certificate_key": {
"type": "string",
"minLength": 1,
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
},
"intermediate_certificate": {
"type": "string",
"minLength": 1,
"example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----"
}
}
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/dns-providers/get.json
================================================
{
"operationId": "getDNSProviders",
"summary": "Get DNS Providers for Certificates",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.view"]
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": "vultr",
"name": "Vultr",
"credentials": "dns_vultr_key = YOUR_VULTR_API_KEY"
},
{
"id": "websupport",
"name": "Websupport.sk",
"credentials": "dns_websupport_identifier = \ndns_websupport_secret_key = "
},
{
"id": "wedos",
"name": "Wedos",
"credentials": "dns_wedos_user = \ndns_wedos_auth = "
},
{
"id": "zoneedit",
"name": "ZoneEdit",
"credentials": "dns_zoneedit_user = \ndns_zoneedit_token = "
}
]
}
},
"schema": {
"$ref": "../../../../components/dns-providers-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/get.json
================================================
{
"operationId": "getCertificates",
"summary": "Get all certificates",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.view"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["owner"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 4,
"created_on": "2024-10-09T05:31:58.000Z",
"modified_on": "2024-10-09T05:32:11.000Z",
"owner_user_id": 1,
"provider": "letsencrypt",
"nice_name": "test.example.com",
"domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z",
"meta": {
"dns_challenge": false
}
}
]
}
},
"schema": {
"$ref": "../../../components/certificate-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/post.json
================================================
{
"operationId": "createCertificate",
"summary": "Create a Certificate",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"requestBody": {
"description": "Certificate Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["provider"],
"properties": {
"provider": {
"$ref": "../../../components/certificate-object.json#/properties/provider"
},
"nice_name": {
"$ref": "../../../components/certificate-object.json#/properties/nice_name"
},
"domain_names": {
"$ref": "../../../components/certificate-object.json#/properties/domain_names"
},
"meta": {
"$ref": "../../../components/certificate-object.json#/properties/meta"
}
}
},
"example": {
"provider": "letsencrypt",
"domain_names": ["test.example.com"],
"meta": {
"dns_challenge": false
}
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires_on": "2025-01-07 04:30:17",
"modified_on": "2024-10-09 05:28:51",
"id": 5,
"created_on": "2024-10-09 05:28:35",
"owner_user_id": 1,
"provider": "letsencrypt",
"nice_name": "test.example.com",
"domain_names": ["test.example.com"],
"meta": {
"dns_challenge": false,
"letsencrypt_certificate": {
"cn": "test.example.com",
"issuer": "C = US, O = Let's Encrypt, CN = E5",
"dates": {
"from": 1728448218,
"to": 1736224217
}
}
}
}
}
},
"schema": {
"$ref": "../../../components/certificate-object.json"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Domains are invalid"
}
}
}
},
"schema": {
"$ref": "../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/test-http/post.json
================================================
{
"operationId": "testHttpReach",
"summary": "Test HTTP Reachability",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.view"]
}
],
"requestBody": {
"description": "Test Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["domains"],
"properties": {
"domains": {
"$ref": "../../../../common.json#/properties/domain_names"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"test.example.org": "ok",
"test.example.com": "other:Invalid domain or IP",
"nonexistent.example.com": "404"
}
}
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/certificates/validate/post.json
================================================
{
"operationId": "validateCertificates",
"summary": "Validates given Custom Certificates",
"tags": ["certificates"],
"security": [
{
"bearerAuth": ["certificates.manage"]
}
],
"requestBody": {
"$ref": "../../../../common.json#/properties/certificate_files"
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"certificate": {
"cn": "mkcert",
"issuer": "O = mkcert development CA, OU = jc@jc-Laptop.local (John Doe), CN = mkcert jc@jc-Laptop.local (John Doe)",
"dates": {
"from": 1728458537,
"to": 1799479337
}
},
"certificate_key": true
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["certificate", "certificate_key"],
"properties": {
"certificate": {
"type": "object",
"additionalProperties": false,
"required": ["cn", "issuer", "dates"],
"properties": {
"cn": {
"type": "string",
"example": "example.com"
},
"issuer": {
"type": "string",
"example": "C = US, O = Let's Encrypt, CN = E5"
},
"dates": {
"type": "object",
"additionalProperties": false,
"required": ["from", "to"],
"properties": {
"from": {
"type": "integer"
},
"to": {
"type": "integer"
}
},
"example": {
"from": 1728448218,
"to": 1736224217
}
}
}
},
"certificate_key": {
"type": "boolean",
"example": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Certificate is not valid"
}
}
}
},
"schema": {
"$ref": "../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/get.json
================================================
{
"operationId": "getDeadHosts",
"summary": "Get all 404 hosts",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.view"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["owner", "certificate"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2024-10-09T01:38:52.000Z",
"modified_on": "2024-10-09T01:38:52.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"certificate_id": 0,
"ssl_forced": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false
}
]
}
},
"schema": {
"$ref": "../../../components/dead-host-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/hostID/delete.json
================================================
{
"operationId": "deleteDeadHost",
"summary": "Delete a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the 404 Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json
================================================
{
"operationId": "disableDeadHost",
"summary": "Disable a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the 404 Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already disabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json
================================================
{
"operationId": "enableDeadHost",
"summary": "Enable a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the 404 Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already enabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/hostID/get.json
================================================
{
"operationId": "getDeadHost",
"summary": "Get a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.view"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the 404 Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T01:38:52.000Z",
"modified_on": "2024-10-09T01:38:52.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"certificate_id": 0,
"ssl_forced": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false
}
}
},
"schema": {
"$ref": "../../../../components/dead-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/hostID/put.json
================================================
{
"operationId": "updateDeadHost",
"summary": "Update a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": ["dead_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the 404 Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"requestBody": {
"description": "404 Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"domain_names": {
"$ref": "../../../../components/dead-host-object.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../../../../components/dead-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../../components/dead-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../../components/dead-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../../components/dead-host-object.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../../../../components/dead-host-object.json#/properties/http2_support"
},
"advanced_config": {
"$ref": "../../../../components/dead-host-object.json#/properties/advanced_config"
},
"meta": {
"$ref": "../../../../components/dead-host-object.json#/properties/meta"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T01:38:52.000Z",
"modified_on": "2024-10-09T01:46:06.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"certificate_id": 0,
"ssl_forced": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"owner": {
"id": 1,
"created_on": "2024-10-09T00:59:56.000Z",
"modified_on": "2024-10-09T00:59:56.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"certificate": null
}
}
},
"schema": {
"$ref": "../../../../components/dead-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/dead-hosts/post.json
================================================
{
"operationId": "create404Host",
"summary": "Create a 404 Host",
"tags": ["404-hosts"],
"security": [
{
"bearerAuth": [
"dead_hosts.manage"
]
}
],
"requestBody": {
"description": "404 Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"domain_names"
],
"properties": {
"domain_names": {
"$ref": "../../../components/dead-host-object.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../../../components/dead-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../components/dead-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../components/dead-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../components/dead-host-object.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../../../components/dead-host-object.json#/properties/http2_support"
},
"advanced_config": {
"$ref": "../../../components/dead-host-object.json#/properties/advanced_config"
},
"meta": {
"$ref": "../../../components/dead-host-object.json#/properties/meta"
}
}
},
"example": {
"domain_names": [
"test.example.com"
],
"certificate_id": 0,
"ssl_forced": false,
"advanced_config": "",
"http2_support": false,
"hsts_enabled": false,
"hsts_subdomains": false,
"meta": {}
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T01:38:52.000Z",
"modified_on": "2024-10-09T01:38:52.000Z",
"owner_user_id": 1,
"domain_names": [
"test.example.com"
],
"certificate_id": 0,
"ssl_forced": false,
"advanced_config": "",
"meta": {},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"certificate": null,
"owner": {
"id": 1,
"created_on": "2024-10-09T00:59:56.000Z",
"modified_on": "2024-10-09T00:59:56.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": [
"admin"
]
}
}
}
},
"schema": {
"$ref": "../../../components/dead-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/get.json
================================================
{
"operationId": "getProxyHosts",
"summary": "Get all proxy hosts",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": [
"proxy_hosts.view"
]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": [
"access_list",
"owner",
"certificate"
]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2025-10-28T01:10:26.000Z",
"modified_on": "2025-10-28T04:07:16.000Z",
"owner_user_id": 1,
"domain_names": [
"test.jc21com"
],
"forward_host": "127.0.0.1",
"forward_port": 8081,
"access_list_id": 1,
"certificate_id": 1,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false
}
]
}
},
"schema": {
"$ref": "../../../components/proxy-host-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/hostID/delete.json
================================================
{
"operationId": "deleteProxyHost",
"summary": "Delete a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": ["proxy_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Proxy Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json
================================================
{
"operationId": "disableProxyHost",
"summary": "Disable a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": ["proxy_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Proxy Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already disabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json
================================================
{
"operationId": "enableProxyHost",
"summary": "Enable a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": ["proxy_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Proxy Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already enabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/hostID/get.json
================================================
{
"operationId": "getProxyHost",
"summary": "Get a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": [
"proxy_hosts.view"
]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Proxy Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 3,
"created_on": "2025-10-30T01:12:05.000Z",
"modified_on": "2025-10-30T01:12:05.000Z",
"owner_user_id": 1,
"domain_names": [
"test.example.com"
],
"forward_host": "127.0.0.1",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
"modified_on": "2025-10-28T00:50:24.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "jamiec",
"nickname": "jamiec",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
]
}
}
}
},
"schema": {
"$ref": "../../../../components/proxy-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/hostID/put.json
================================================
{
"operationId": "updateProxyHost",
"summary": "Update a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": [
"proxy_hosts.manage"
]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Proxy Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"requestBody": {
"description": "Proxy Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"domain_names": {
"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
},
"forward_scheme": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
},
"forward_host": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
},
"forward_port": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
},
"certificate_id": {
"$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto": {
"$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"http2_support": {
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits"
},
"caching_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
},
"allow_websocket_upgrade": {
"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
},
"access_list_id": {
"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
},
"advanced_config": {
"$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config"
},
"enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/enabled"
},
"meta": {
"$ref": "../../../../components/proxy-host-object.json#/properties/meta"
},
"locations": {
"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 3,
"created_on": "2025-10-30T01:12:05.000Z",
"modified_on": "2025-10-30T01:17:06.000Z",
"owner_user_id": 1,
"domain_names": [
"test.example.com"
],
"forward_host": "127.0.0.1",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
"modified_on": "2025-10-28T00:50:24.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "jamiec",
"nickname": "jamiec",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
]
},
"certificate": null,
"access_list": null
}
}
},
"schema": {
"$ref": "../../../../components/proxy-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/proxy-hosts/post.json
================================================
{
"operationId": "createProxyHost",
"summary": "Create a Proxy Host",
"tags": ["proxy-hosts"],
"security": [
{
"bearerAuth": [
"proxy_hosts.manage"
]
}
],
"requestBody": {
"description": "Proxy Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"domain_names",
"forward_scheme",
"forward_host",
"forward_port"
],
"properties": {
"domain_names": {
"$ref": "../../../components/proxy-host-object.json#/properties/domain_names"
},
"forward_scheme": {
"$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme"
},
"forward_host": {
"$ref": "../../../components/proxy-host-object.json#/properties/forward_host"
},
"forward_port": {
"$ref": "../../../components/proxy-host-object.json#/properties/forward_port"
},
"certificate_id": {
"$ref": "../../../components/proxy-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto": {
"$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"http2_support": {
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../../../components/proxy-host-object.json#/properties/block_exploits"
},
"caching_enabled": {
"$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled"
},
"allow_websocket_upgrade": {
"$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
},
"access_list_id": {
"$ref": "../../../components/proxy-host-object.json#/properties/access_list_id"
},
"advanced_config": {
"$ref": "../../../components/proxy-host-object.json#/properties/advanced_config"
},
"enabled": {
"$ref": "../../../components/proxy-host-object.json#/properties/enabled"
},
"meta": {
"$ref": "../../../components/proxy-host-object.json#/properties/meta"
},
"locations": {
"$ref": "../../../components/proxy-host-object.json#/properties/locations"
}
}
},
"example": {
"domain_names": [
"test.example.com"
],
"forward_scheme": "http",
"forward_host": "127.0.0.1",
"forward_port": 8080
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 3,
"created_on": "2025-10-30T01:12:05.000Z",
"modified_on": "2025-10-30T01:12:05.000Z",
"owner_user_id": 1,
"domain_names": [
"test.example.com"
],
"forward_host": "127.0.0.1",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"meta": {},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"certificate": null,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
"modified_on": "2025-10-28T00:50:24.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "jamiec",
"nickname": "jamiec",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
]
},
"access_list": null
}
}
},
"schema": {
"$ref": "../../../components/proxy-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/get.json
================================================
{
"operationId": "getRedirectionHosts",
"summary": "Get all Redirection hosts",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.view"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["owner", "certificate"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2024-10-09T01:13:12.000Z",
"modified_on": "2024-10-09T01:13:13.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"forward_domain_name": "something-else.com",
"preserve_path": false,
"certificate_id": 0,
"ssl_forced": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"forward_scheme": "http",
"forward_http_code": 301
}
]
}
},
"schema": {
"$ref": "../../../components/redirection-host-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/hostID/delete.json
================================================
{
"operationId": "deleteRedirectionHost",
"summary": "Delete a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Redirection Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json
================================================
{
"operationId": "disableRedirectionHost",
"summary": "Disable a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Redirection Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already disabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json
================================================
{
"operationId": "enableRedirectionHost",
"summary": "Enable a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Redirection Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already enabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/hostID/get.json
================================================
{
"operationId": "getRedirectionHost",
"summary": "Get a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.view"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Redirection Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T01:13:12.000Z",
"modified_on": "2024-10-09T01:13:13.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"forward_domain_name": "something-else.com",
"preserve_path": false,
"certificate_id": 0,
"ssl_forced": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"forward_scheme": "http",
"forward_http_code": 301
}
}
},
"schema": {
"$ref": "../../../../components/redirection-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/hostID/put.json
================================================
{
"operationId": "updateRedirectionHost",
"summary": "Update a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": ["redirection_hosts.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "hostID",
"description": "The ID of the Redirection Host",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"requestBody": {
"description": "Redirection Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"domain_names": {
"$ref": "../../../../components/redirection-host-object.json#/properties/domain_names"
},
"forward_http_code": {
"$ref": "../../../../components/redirection-host-object.json#/properties/forward_http_code"
},
"forward_scheme": {
"$ref": "../../../../components/redirection-host-object.json#/properties/forward_scheme"
},
"forward_domain_name": {
"$ref": "../../../../components/redirection-host-object.json#/properties/forward_domain_name"
},
"preserve_path": {
"$ref": "../../../../components/redirection-host-object.json#/properties/preserve_path"
},
"certificate_id": {
"$ref": "../../../../components/redirection-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../../components/redirection-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../../components/redirection-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../../components/redirection-host-object.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../../../../components/redirection-host-object.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../../../../components/redirection-host-object.json#/properties/block_exploits"
},
"advanced_config": {
"$ref": "../../../../components/redirection-host-object.json#/properties/advanced_config"
},
"meta": {
"$ref": "../../../../components/redirection-host-object.json#/properties/meta"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T01:13:12.000Z",
"modified_on": "2024-10-09T01:18:11.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"forward_domain_name": "something-else.com",
"preserve_path": false,
"certificate_id": 0,
"ssl_forced": false,
"block_exploits": false,
"advanced_config": "",
"meta": {
"nginx_online": true,
"nginx_err": null
},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"forward_scheme": "http",
"forward_http_code": 301,
"owner": {
"id": 1,
"created_on": "2024-10-09T00:59:56.000Z",
"modified_on": "2024-10-09T00:59:56.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"certificate": null
}
}
},
"schema": {
"$ref": "../../../../components/redirection-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/redirection-hosts/post.json
================================================
{
"operationId": "createRedirectionHost",
"summary": "Create a Redirection Host",
"tags": ["redirection-hosts"],
"security": [
{
"bearerAuth": [
"redirection_hosts.manage"
]
}
],
"requestBody": {
"description": "Redirection Host Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"domain_names",
"forward_scheme",
"forward_http_code",
"forward_domain_name"
],
"properties": {
"domain_names": {
"$ref": "../../../components/redirection-host-object.json#/properties/domain_names"
},
"forward_http_code": {
"$ref": "../../../components/redirection-host-object.json#/properties/forward_http_code"
},
"forward_scheme": {
"$ref": "../../../components/redirection-host-object.json#/properties/forward_scheme"
},
"forward_domain_name": {
"$ref": "../../../components/redirection-host-object.json#/properties/forward_domain_name"
},
"preserve_path": {
"$ref": "../../../components/redirection-host-object.json#/properties/preserve_path"
},
"certificate_id": {
"$ref": "../../../components/redirection-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../components/redirection-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../components/redirection-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../components/redirection-host-object.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../../../components/redirection-host-object.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../../../components/redirection-host-object.json#/properties/block_exploits"
},
"advanced_config": {
"$ref": "../../../components/redirection-host-object.json#/properties/advanced_config"
},
"meta": {
"$ref": "../../../components/redirection-host-object.json#/properties/meta"
}
}
},
"example": {
"domain_names": [
"test.example.com"
],
"forward_domain_name": "example.com",
"forward_scheme": "auto",
"forward_http_code": 301,
"preserve_path": false,
"block_exploits": false,
"certificate_id": 0,
"ssl_forced": false,
"http2_support": false,
"hsts_enabled": false,
"hsts_subdomains": false,
"advanced_config": "",
"meta": {}
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 2,
"created_on": "2025-10-30T01:27:04.000Z",
"modified_on": "2025-10-30T01:27:04.000Z",
"owner_user_id": 1,
"domain_names": [
"test.example.com"
],
"forward_domain_name": "example.com",
"preserve_path": false,
"certificate_id": 0,
"ssl_forced": false,
"block_exploits": false,
"advanced_config": "",
"meta": {},
"http2_support": false,
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"forward_scheme": "auto",
"forward_http_code": 301,
"certificate": null,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
"modified_on": "2025-10-28T00:50:24.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "jamiec",
"nickname": "jamiec",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
]
}
}
}
},
"schema": {
"$ref": "../../../components/redirection-host-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/get.json
================================================
{
"operationId": "getStreams",
"summary": "Get all streams",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.view"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["owner", "certificate"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2024-10-09T02:33:45.000Z",
"modified_on": "2024-10-09T02:33:45.000Z",
"owner_user_id": 1,
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
}
]
}
},
"schema": {
"$ref": "../../../components/stream-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/post.json
================================================
{
"operationId": "createStream",
"summary": "Create a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": [
"streams.manage"
]
}
],
"requestBody": {
"description": "Stream Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"incoming_port",
"forwarding_host",
"forwarding_port"
],
"properties": {
"incoming_port": {
"$ref": "../../../components/stream-object.json#/properties/incoming_port"
},
"forwarding_host": {
"$ref": "../../../components/stream-object.json#/properties/forwarding_host"
},
"forwarding_port": {
"$ref": "../../../components/stream-object.json#/properties/forwarding_port"
},
"tcp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/tcp_forwarding"
},
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
},
"domain_names": {
"$ref": "../../../components/dead-host-object.json#/properties/domain_names"
}
}
},
"example": {
"incoming_port": 8888,
"forwarding_host": "127.0.0.1",
"forwarding_port": 8080,
"tcp_forwarding": true,
"udp_forwarding": false,
"certificate_id": 0,
"meta": {}
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T02:33:45.000Z",
"modified_on": "2024-10-09T02:33:45.000Z",
"owner_user_id": 1,
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"owner": {
"id": 1,
"created_on": "2024-10-09T02:33:16.000Z",
"modified_on": "2024-10-09T02:33:16.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": [
"admin"
]
},
"certificate_id": 0
}
}
},
"schema": {
"$ref": "../../../components/stream-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/streamID/delete.json
================================================
{
"operationId": "deleteStream",
"summary": "Delete a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "streamID",
"description": "The ID of the Stream",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/streamID/disable/post.json
================================================
{
"operationId": "disableStream",
"summary": "Disable a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "streamID",
"description": "The ID of the Stream",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already disabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/streamID/enable/post.json
================================================
{
"operationId": "enableStream",
"summary": "Enable a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "streamID",
"description": "The ID of the Stream",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Host is already enabled"
}
}
}
},
"schema": {
"$ref": "../../../../../components/error.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/streamID/get.json
================================================
{
"operationId": "getStream",
"summary": "Get a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.view"]
}
],
"parameters": [
{
"in": "path",
"name": "streamID",
"description": "The ID of the Stream",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T02:33:45.000Z",
"modified_on": "2024-10-09T02:33:45.000Z",
"owner_user_id": 1,
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
}
}
},
"schema": {
"$ref": "../../../../components/stream-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/nginx/streams/streamID/put.json
================================================
{
"operationId": "updateStream",
"summary": "Update a Stream",
"tags": ["streams"],
"security": [
{
"bearerAuth": ["streams.manage"]
}
],
"parameters": [
{
"in": "path",
"name": "streamID",
"description": "The ID of the Stream",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 2
}
],
"requestBody": {
"description": "Stream Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"incoming_port": {
"$ref": "../../../../components/stream-object.json#/properties/incoming_port"
},
"forwarding_host": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
},
"forwarding_port": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
},
"tcp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
},
"udp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
},
"certificate_id": {
"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../../components/stream-object.json#/properties/meta"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2024-10-09T02:33:45.000Z",
"modified_on": "2024-10-09T02:33:45.000Z",
"owner_user_id": 1,
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
"enabled": true,
"owner": {
"id": 1,
"created_on": "2024-10-09T02:33:16.000Z",
"modified_on": "2024-10-09T02:33:16.000Z",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
},
"certificate_id": 0
}
}
},
"schema": {
"$ref": "../../../../components/stream-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/reports/hosts/get.json
================================================
{
"operationId": "reportsHosts",
"summary": "Report on Host Statistics",
"tags": ["reports"],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"proxy": 20,
"redirection": 1,
"stream": 0,
"dead": 1
}
}
},
"schema": {
"type": "object",
"properties": {
"proxy": {
"type": "integer",
"description": "Proxy Hosts Count",
"example": 20
},
"redirection": {
"type": "integer",
"description": "Redirection Hosts Count",
"example": 2
},
"stream": {
"type": "integer",
"description": "Streams Count",
"example": 0
},
"dead": {
"type": "integer",
"description": "404 Hosts Count",
"example": 3
}
}
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/schema/get.json
================================================
{
"operationId": "schema",
"summary": "Returns this swagger API schema",
"tags": ["public"],
"responses": {
"200": {
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/settings/get.json
================================================
{
"operationId": "getSettings",
"summary": "Get all settings",
"tags": ["settings"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": "default-site",
"name": "Default Site",
"description": "What to show when Nginx is hit with an unknown Host",
"value": "congratulations",
"meta": {}
}
]
}
},
"schema": {
"$ref": "../../components/setting-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/settings/settingID/get.json
================================================
{
"operationId": "getSetting",
"summary": "Get a setting",
"tags": ["settings"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "settingID",
"schema": {
"type": "string",
"minLength": 1
},
"required": true,
"description": "Setting ID",
"example": "default-site"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": "default-site",
"name": "Default Site",
"description": "What to show when Nginx is hit with an unknown Host",
"value": "congratulations",
"meta": {}
}
}
},
"schema": {
"$ref": "../../../components/setting-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/settings/settingID/put.json
================================================
{
"operationId": "updateSetting",
"summary": "Update a setting",
"tags": ["settings"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "settingID",
"schema": {
"type": "string",
"minLength": 1,
"enum": ["default-site"]
},
"required": true,
"description": "Setting ID",
"example": "default-site"
}
],
"requestBody": {
"description": "Setting Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"value": {
"type": "string",
"minLength": 1,
"enum": ["congratulations", "404", "444", "redirect", "html"],
"example": "html"
},
"meta": {
"type": "object",
"additionalProperties": false,
"properties": {
"redirect": {
"type": "string"
},
"html": {
"type": "string"
}
},
"example": {
"html": "hello world
"
}
}
}
},
"example": {
"value": "congratulations",
"meta": {}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": "default-site",
"name": "Default Site",
"description": "What to show when Nginx is hit with an unknown Host",
"value": "congratulations",
"meta": {}
}
}
},
"schema": {
"$ref": "../../../components/setting-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/tokens/2fa/post.json
================================================
{
"operationId": "loginWith2FA",
"summary": "Verify 2FA code and get full token",
"tags": ["tokens"],
"requestBody": {
"description": "2fa Challenge Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"challenge_token": {
"minLength": 1,
"type": "string",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
},
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "012345"
}
},
"required": ["challenge_token", "code"],
"type": "object"
},
"example": {
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"code": "012345"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"$ref": "../../../components/token-object.json"
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/tokens/get.json
================================================
{
"operationId": "refreshToken",
"summary": "Refresh your access token",
"tags": ["tokens"],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"$ref": "../../components/token-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/tokens/post.json
================================================
{
"operationId": "requestToken",
"summary": "Request a new access token from credentials",
"tags": ["tokens"],
"requestBody": {
"description": "Credentials Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"identity": {
"minLength": 1,
"type": "string",
"example": "me@example.com"
},
"scope": {
"minLength": 1,
"type": "string",
"enum": ["user"],
"example": "user"
},
"secret": {
"minLength": 1,
"type": "string",
"example": "bigredhorsebanana"
}
},
"required": ["identity", "secret"],
"type": "object"
},
"example": {
"identity": "me@example.com",
"secret": "bigredhorsebanana"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"oneOf": [
{
"$ref": "../../components/token-object.json"
},
{
"$ref": "../../components/token-challenge.json"
}
]
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/users/get.json
================================================
{
"operationId": "getUsers",
"summary": "Get all users",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "query",
"name": "expand",
"description": "Expansions",
"schema": {
"type": "string",
"enum": ["permissions"]
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": [
{
"id": 1,
"created_on": "2020-01-30T09:36:08.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie Curnow",
"nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"]
}
]
},
"withPermissions": {
"value": [
{
"id": 1,
"created_on": "2020-01-30T09:36:08.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie Curnow",
"nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"],
"permissions": {
"visibility": "all",
"proxy_hosts": "manage",
"redirection_hosts": "manage",
"dead_hosts": "manage",
"streams": "manage",
"access_lists": "manage",
"certificates": "manage"
}
}
]
}
},
"schema": {
"$ref": "../../components/user-list.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/post.json
================================================
{
"operationId": "createUser",
"summary": "Create a User",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"requestBody": {
"description": "User Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["name", "nickname", "email"],
"properties": {
"name": {
"$ref": "../../components/user-object.json#/properties/name"
},
"nickname": {
"$ref": "../../components/user-object.json#/properties/nickname"
},
"email": {
"$ref": "../../components/user-object.json#/properties/email"
},
"roles": {
"$ref": "../../components/user-object.json#/properties/roles"
},
"is_disabled": {
"$ref": "../../components/user-object.json#/properties/is_disabled"
},
"auth": {
"type": "object",
"description": "Auth Credentials",
"example": {
"type": "password",
"secret": "bigredhorsebanana"
}
}
}
}
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 2,
"created_on": "2020-01-30T09:41:04.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie Curnow",
"nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"],
"permissions": {
"id": 3,
"created_on": "2020-01-30T09:41:04.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"user_id": 2,
"visibility": "user",
"proxy_hosts": "manage",
"redirection_hosts": "manage",
"dead_hosts": "manage",
"streams": "manage",
"access_lists": "manage",
"certificates": "manage"
}
}
}
},
"schema": {
"$ref": "../../components/user-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/2fa/backup-codes/post.json
================================================
{
"operationId": "regenUser2faCodes",
"summary": "Regenerate 2FA backup codes",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/users/userID/2fa/delete.json
================================================
{
"operationId": "disableUser2fa",
"summary": "Disable 2fa for user",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
},
{
"in": "query",
"name": "code",
"schema": {
"type": "string",
"minLength": 6,
"maxLength": 6,
"example": "012345"
},
"required": true,
"description": "2fa Code",
"example": "012345"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/users/userID/2fa/enable/post.json
================================================
{
"operationId": "enableUser2fa",
"summary": "Verify code and enable 2FA",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/users/userID/2fa/get.json
================================================
{
"operationId": "getUser2faStatus",
"summary": "Get user 2fa Status",
"tags": ["users"],
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"enabled": false,
"backup_codes_remaining": 0
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "backup_codes_remaining"],
"properties": {
"enabled": {
"type": "boolean",
"description": "Is 2FA enabled for this user",
"example": true
},
"backup_codes_remaining": {
"type": "integer",
"description": "Number of remaining backup codes for this user",
"example": 5
}
}
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/2fa/post.json
================================================
{
"operationId": "setupUser2fa",
"summary": "Start 2FA setup, returns QR code URL",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"secret": "JZYCEBIEEJYUGPQM",
"otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager"
}
}
},
"schema": {
"type": "object",
"required": ["secret", "otpauth_url"],
"additionalProperties": false,
"properties": {
"secret": {
"description": "TOTP Secret",
"example": "JZYCEBIEEJYUGPQM",
"type": "string"
},
"otpauth_url": {
"description": "OTP Auth URL for QR Code generation",
"example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager",
"type": "string"
}
}
}
}
},
"description": "200 response"
}
}
}
================================================
FILE: backend/schema/paths/users/userID/auth/put.json
================================================
{
"operationId": "updateUserAuth",
"summary": "Update a User's Authentication",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"oneOf": [
{
"type": "string",
"pattern": "^me$"
},
{
"type": "integer",
"minimum": 1
}
]
},
"required": true,
"description": "User ID or 'me' for yourself",
"example": 2
}
],
"requestBody": {
"description": "Auth Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["type", "secret"],
"properties": {
"type": {
"type": "string",
"pattern": "^password$",
"example": "password"
},
"current": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"example": "changeme"
},
"secret": {
"type": "string",
"minLength": 8,
"maxLength": 64,
"example": "mySuperN3wP@ssword!"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/delete.json
================================================
{
"operationId": "deleteUser",
"summary": "Delete a User",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/get.json
================================================
{
"operationId": "getUser",
"summary": "Get a user",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"oneOf": [
{
"type": "string",
"pattern": "^me$"
},
{
"type": "integer",
"minimum": 1
}
]
},
"required": true,
"description": "User ID or 'me' for yourself",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2020-01-30T09:36:08.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie Curnow",
"nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"]
}
}
},
"schema": {
"$ref": "../../../components/user-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/login/post.json
================================================
{
"operationId": "loginAsUser",
"summary": "Login as this user",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg",
"expires": "2020-01-31T10:56:23.239Z",
"user": {
"id": 1,
"created_on": "2020-01-30T10:43:44.000Z",
"modified_on": "2020-01-30T10:43:44.000Z",
"is_disabled": false,
"email": "user2@example.com",
"name": "John Doe",
"nickname": "Jonny",
"avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm",
"roles": []
}
}
}
},
"schema": {
"type": "object",
"description": "Login object",
"required": ["expires", "token", "user"],
"additionalProperties": false,
"properties": {
"token": {
"description": "JWT Token",
"type": "string",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
},
"expires": {
"description": "Token Expiry Timestamp",
"type": "string",
"example": "2020-01-30T10:43:44.000Z"
},
"user": {
"$ref": "../../../../components/user-object.json"
}
}
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/permissions/put.json
================================================
{
"operationId": "updateUserPermissions",
"summary": "Update a User's Permissions",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Permissions Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../../../components/permission-object.json"
},
"example": {
"visibility": "all",
"access_lists": "view",
"certificates": "hidden",
"dead_hosts": "hidden",
"proxy_hosts": "manage",
"redirection_hosts": "hidden",
"streams": "hidden"
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/users/userID/put.json
================================================
{
"operationId": "updateUser",
"summary": "Update a User",
"tags": ["users"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"oneOf": [
{
"type": "string",
"pattern": "^me$"
},
{
"type": "integer",
"minimum": 1
}
]
},
"required": true,
"description": "User ID or 'me' for yourself",
"example": 2
}
],
"requestBody": {
"description": "User Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": {
"$ref": "../../../components/user-object.json#/properties/name"
},
"nickname": {
"$ref": "../../../components/user-object.json#/properties/nickname"
},
"email": {
"$ref": "../../../components/user-object.json#/properties/email"
},
"roles": {
"$ref": "../../../components/user-object.json#/properties/roles"
},
"is_disabled": {
"$ref": "../../../components/user-object.json#/properties/is_disabled"
}
}
}
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 2,
"created_on": "2020-01-30T09:36:08.000Z",
"modified_on": "2020-01-30T09:41:04.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie Curnow",
"nickname": "James",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": ["admin"]
}
}
},
"schema": {
"$ref": "../../../components/user-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/paths/version/check/get.json
================================================
{
"operationId": "checkVersion",
"summary": "Returns any new version data from github",
"tags": ["public"],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"current": "v2.12.0",
"latest": "v2.13.4",
"update_available": true
}
}
},
"schema": {
"$ref": "../../../components/check-version-object.json"
}
}
}
}
}
}
================================================
FILE: backend/schema/swagger.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "Nginx Proxy Manager API",
"version": "2.x.x",
"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)."
},
"servers": [
{
"url": "http://127.0.0.1:81/api"
}
],
"components": {
"securitySchemes": {
"$ref": "./components/security-schemes.json"
}
},
"tags": [
{
"name": "public",
"description": "Endpoints that do not require authentication"
},
{
"name": "audit-log",
"description": "Endpoints related to Audit Logs"
},
{
"name": "access-lists",
"description": "Endpoints related to Access Lists"
},
{
"name": "certificates",
"description": "Endpoints related to Certificates"
},
{
"name": "404-hosts",
"description": "Endpoints related to 404 Hosts"
},
{
"name": "proxy-hosts",
"description": "Endpoints related to Proxy Hosts"
},
{
"name": "redirection-hosts",
"description": "Endpoints related to Redirection Hosts"
},
{
"name": "streams",
"description": "Endpoints related to Streams"
},
{
"name": "reports",
"description": "Endpoints for viewing reports"
},
{
"name": "settings",
"description": "Endpoints for managing application settings"
},
{
"name": "tokens",
"description": "Endpoints for managing authentication tokens"
},
{
"name": "users",
"description": "Endpoints for managing users"
}
],
"paths": {
"/": {
"get": {
"$ref": "./paths/get.json"
}
},
"/audit-log": {
"get": {
"$ref": "./paths/audit-log/get.json"
}
},
"/audit-log/{id}": {
"get": {
"$ref": "./paths/audit-log/id/get.json"
}
},
"/nginx/access-lists": {
"get": {
"$ref": "./paths/nginx/access-lists/get.json"
},
"post": {
"$ref": "./paths/nginx/access-lists/post.json"
}
},
"/nginx/access-lists/{listID}": {
"get": {
"$ref": "./paths/nginx/access-lists/listID/get.json"
},
"put": {
"$ref": "./paths/nginx/access-lists/listID/put.json"
},
"delete": {
"$ref": "./paths/nginx/access-lists/listID/delete.json"
}
},
"/nginx/certificates": {
"get": {
"$ref": "./paths/nginx/certificates/get.json"
},
"post": {
"$ref": "./paths/nginx/certificates/post.json"
}
},
"/nginx/certificates/dns-providers": {
"get": {
"$ref": "./paths/nginx/certificates/dns-providers/get.json"
}
},
"/nginx/certificates/validate": {
"post": {
"$ref": "./paths/nginx/certificates/validate/post.json"
}
},
"/nginx/certificates/test-http": {
"post": {
"$ref": "./paths/nginx/certificates/test-http/post.json"
}
},
"/nginx/certificates/{certID}": {
"get": {
"$ref": "./paths/nginx/certificates/certID/get.json"
},
"delete": {
"$ref": "./paths/nginx/certificates/certID/delete.json"
}
},
"/nginx/certificates/{certID}/download": {
"get": {
"$ref": "./paths/nginx/certificates/certID/download/get.json"
}
},
"/nginx/certificates/{certID}/renew": {
"post": {
"$ref": "./paths/nginx/certificates/certID/renew/post.json"
}
},
"/nginx/certificates/{certID}/upload": {
"post": {
"$ref": "./paths/nginx/certificates/certID/upload/post.json"
}
},
"/nginx/proxy-hosts": {
"get": {
"$ref": "./paths/nginx/proxy-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/proxy-hosts/post.json"
}
},
"/nginx/proxy-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/proxy-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/proxy-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/proxy-hosts/hostID/delete.json"
}
},
"/nginx/proxy-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json"
}
},
"/nginx/proxy-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json"
}
},
"/nginx/redirection-hosts": {
"get": {
"$ref": "./paths/nginx/redirection-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/redirection-hosts/post.json"
}
},
"/nginx/redirection-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/redirection-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/redirection-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/redirection-hosts/hostID/delete.json"
}
},
"/nginx/redirection-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json"
}
},
"/nginx/redirection-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json"
}
},
"/nginx/dead-hosts": {
"get": {
"$ref": "./paths/nginx/dead-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/dead-hosts/post.json"
}
},
"/nginx/dead-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/dead-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/dead-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/dead-hosts/hostID/delete.json"
}
},
"/nginx/dead-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json"
}
},
"/nginx/dead-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json"
}
},
"/nginx/streams": {
"get": {
"$ref": "./paths/nginx/streams/get.json"
},
"post": {
"$ref": "./paths/nginx/streams/post.json"
}
},
"/nginx/streams/{streamID}": {
"get": {
"$ref": "./paths/nginx/streams/streamID/get.json"
},
"put": {
"$ref": "./paths/nginx/streams/streamID/put.json"
},
"delete": {
"$ref": "./paths/nginx/streams/streamID/delete.json"
}
},
"/nginx/streams/{streamID}/enable": {
"post": {
"$ref": "./paths/nginx/streams/streamID/enable/post.json"
}
},
"/nginx/streams/{streamID}/disable": {
"post": {
"$ref": "./paths/nginx/streams/streamID/disable/post.json"
}
},
"/reports/hosts": {
"get": {
"$ref": "./paths/reports/hosts/get.json"
}
},
"/schema": {
"get": {
"$ref": "./paths/schema/get.json"
}
},
"/settings": {
"get": {
"$ref": "./paths/settings/get.json"
}
},
"/settings/{settingID}": {
"get": {
"$ref": "./paths/settings/settingID/get.json"
},
"put": {
"$ref": "./paths/settings/settingID/put.json"
}
},
"/tokens": {
"get": {
"$ref": "./paths/tokens/get.json"
},
"post": {
"$ref": "./paths/tokens/post.json"
}
},
"/tokens/2fa": {
"post": {
"$ref": "./paths/tokens/2fa/post.json"
}
},
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
}
},
"/users": {
"get": {
"$ref": "./paths/users/get.json"
},
"post": {
"$ref": "./paths/users/post.json"
}
},
"/users/{userID}": {
"get": {
"$ref": "./paths/users/userID/get.json"
},
"put": {
"$ref": "./paths/users/userID/put.json"
},
"delete": {
"$ref": "./paths/users/userID/delete.json"
}
},
"/users/{userID}/2fa": {
"post": {
"$ref": "./paths/users/userID/2fa/post.json"
},
"get": {
"$ref": "./paths/users/userID/2fa/get.json"
},
"delete": {
"$ref": "./paths/users/userID/2fa/delete.json"
}
},
"/users/{userID}/2fa/enable": {
"post": {
"$ref": "./paths/users/userID/2fa/enable/post.json"
}
},
"/users/{userID}/2fa/backup-codes": {
"post": {
"$ref": "./paths/users/userID/2fa/backup-codes/post.json"
}
},
"/users/{userID}/auth": {
"put": {
"$ref": "./paths/users/userID/auth/put.json"
}
},
"/users/{userID}/permissions": {
"put": {
"$ref": "./paths/users/userID/permissions/put.json"
}
},
"/users/{userID}/login": {
"post": {
"$ref": "./paths/users/userID/login/post.json"
}
}
}
}
================================================
FILE: backend/scripts/install-certbot-plugins
================================================
#!/usr/bin/node
// Usage:
// Install all plugins defined in `../certbot/dns-plugins.json`:
// ./install-certbot-plugins
// Install one or more specific plugins:
// ./install-certbot-plugins route53 cloudflare
//
// Usage with a running docker container:
// docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins"
//
import batchflow from "batchflow";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
import { installPlugin } from "../lib/certbot.js";
import { certbot as logger } from "../logger.js";
let hasErrors = false;
const failingPlugins = [];
let pluginKeys = Object.keys(dnsPlugins);
if (process.argv.length > 2) {
pluginKeys = process.argv.slice(2);
}
batchflow(pluginKeys)
.sequential()
.each((i, pluginKey, next) => {
installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
failingPlugins.push(pluginKey);
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
logger.error(
"Some plugins failed to install. Please check the logs above. Failing plugins: " +
"\n - " +
failingPlugins.join("\n - "),
);
process.exit(1);
} else {
logger.complete("Plugins installed successfully");
process.exit(0);
}
});
================================================
FILE: backend/scripts/regenerate-config
================================================
#!/usr/bin/env node
import * as process from "node:process"; // Use the node: protocol for built-ins
import internalNginx from "../internal/nginx.js";
import { global as logger } from "../logger.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
import streamModel from "../models/stream.js";
const args = process.argv.slice(2);
const UNATTENDED = args.includes("-y") || args.includes("--yes");
const DRY_RUN = args.includes("--dry-run");
if (args.includes("--help") || args.includes("-h")) {
console.log("\nThis will iterate over all Hosts and regnerate their Nginx configs.\n")
console.log("Usage: ./regenerate-config [-h|--help] [-y|--yes] [--dry-run]\n");
process.exit(0);
}
// ask for the user to confirm the action if not in unattended mode
if (!UNATTENDED && !DRY_RUN) {
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query) =>
new Promise((resolve) => rl.question(query, resolve));
const answer = await question(
"This will iterate over all Hosts and regnerate their Nginx configs.\n\nAre you sure you want to proceed? (y/N) ",
);
rl.close();
if (answer.toLowerCase() !== "y") {
console.log("Aborting.");
process.exit(0);
}
}
const logIt = (msg, type = "info") => logger[type](
`${DRY_RUN ? '[DRY RUN] ' : ''}${msg}`,
);
// Let's do it.
const processItems = async (model, type) => {
const rows = await model
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph(model.defaultAllowGraph)
.withGraphFetched(`[${model.defaultExpand.join(", ")}]`)
.orderBy(...model.defaultOrder);
logIt(`[${type}] Found ${rows.length} rows to process...`);
for (const row of rows) {
if (!DRY_RUN) {
logIt(`[${type}] Regenerating config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`);
await internalNginx.configure(proxyHostModel, "proxy_host", row);
} else {
logIt(`[${type}] Skipping generation of config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`);
}
}
};
await processItems(proxyHostModel, "Proxy Host");
await processItems(redirectionHostModel, "Redirection Host");
await processItems(deadHostModel, "404 Host");
await processItems(streamModel, "Stream");
logIt("Completed", "success");
process.exit(0);
================================================
FILE: backend/setup.js
================================================
import { installPlugins } from "./lib/certbot.js";
import utils from "./lib/utils.js";
import { setup as logger } from "./logger.js";
import authModel from "./models/auth.js";
import certificateModel from "./models/certificate.js";
import settingModel from "./models/setting.js";
import userModel from "./models/user.js";
import userPermissionModel from "./models/user_permission.js";
export const isSetup = async () => {
const row = await userModel.query().select("id").where("is_deleted", 0).first();
return row?.id > 0;
}
/**
* Creates a default admin users if one doesn't already exist in the database
*
* @returns {Promise}
*/
const setupDefaultUser = async () => {
const initialAdminEmail = process.env.INITIAL_ADMIN_EMAIL;
const initialAdminPassword = process.env.INITIAL_ADMIN_PASSWORD;
// This will only create a new user when there are no active users in the database
// and the INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD environment variables are set.
// Otherwise, users should be shown the setup wizard in the frontend.
// I'm keeping this legacy behavior in case some people are automating deployments.
if (!initialAdminEmail || !initialAdminPassword) {
return Promise.resolve();
}
const userIsetup = await isSetup();
if (!userIsetup) {
// Create a new user and set password
logger.info(`Creating a new user: ${initialAdminEmail} with password: ${initialAdminPassword}`);
const data = {
is_deleted: 0,
email: initialAdminEmail,
name: "Administrator",
nickname: "Admin",
avatar: "",
roles: ["admin"],
};
const user = await userModel
.query()
.insertAndFetch(data);
await authModel
.query()
.insert({
user_id: user.id,
type: "password",
secret: initialAdminPassword,
meta: {},
});
await userPermissionModel.query().insert({
user_id: user.id,
visibility: "all",
proxy_hosts: "manage",
redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
});
logger.info("Initial admin setup completed");
}
};
/**
* Creates default settings if they don't already exist in the database
*
* @returns {Promise}
*/
const setupDefaultSettings = async () => {
const row = await settingModel
.query()
.select("id")
.where({ id: "default-site" })
.first();
if (!row?.id) {
await settingModel
.query()
.insert({
id: "default-site",
name: "Default Site",
description: "What to show when Nginx is hit with an unknown Host",
value: "congratulations",
meta: {},
});
logger.info("Default settings added");
}
};
/**
* Installs all Certbot plugins which are required for an installed certificate
*
* @returns {Promise}
*/
const setupCertbotPlugins = async () => {
const certificates = await certificateModel
.query()
.where("is_deleted", 0)
.andWhere("provider", "letsencrypt");
if (certificates?.length) {
const plugins = [];
const promises = [];
certificates.map((certificate) => {
if (certificate.meta && certificate.meta.dns_challenge === true) {
if (plugins.indexOf(certificate.meta.dns_provider) === -1) {
plugins.push(certificate.meta.dns_provider);
}
// Make sure credentials file exists
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
// Escape single quotes and backslashes
if (typeof certificate.meta.dns_provider_credentials === "string") {
const escapedCredentials = certificate.meta.dns_provider_credentials
.replaceAll("'", "\\'")
.replaceAll("\\", "\\\\");
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
promises.push(utils.exec(credentials_cmd));
}
}
return true;
});
await installPlugins(plugins);
if (promises.length) {
await Promise.all(promises);
logger.info(`Added Certbot plugins ${plugins.join(", ")}`);
}
}
};
/**
* Starts a timer to call run the logrotation binary every two days
* @returns {Promise}
*/
const setupLogrotation = () => {
const intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days
const runLogrotate = async () => {
try {
await utils.exec("logrotate /etc/logrotate.d/nginx-proxy-manager");
logger.info("Logrotate completed.");
} catch (e) {
logger.warn(e);
}
};
logger.info("Logrotate Timer initialized");
setInterval(runLogrotate, intervalTimeout);
// And do this now as well
return runLogrotate();
};
export default () => setupDefaultUser().then(setupDefaultSettings).then(setupCertbotPlugins).then(setupLogrotation);
================================================
FILE: backend/templates/_access.conf
================================================
{% if access_list_id > 0 %}
{% if access_list.items.length > 0 %}
# Authorization
auth_basic "Authorization required";
auth_basic_user_file /data/access/{{ access_list_id }};
{% if access_list.pass_auth == 0 or access_list.pass_auth == false %}
proxy_set_header Authorization "";
{% endif %}
{% endif %}
# Access Rules: {{ access_list.clients | size }} total
{% for client in access_list.clients %}
{{client | nginxAccessRule}}
{% endfor %}
deny all;
# Access checks must...
{% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %}
satisfy any;
{% else %}
satisfy all;
{% endif %}
{% endif %}
================================================
FILE: backend/templates/_assets.conf
================================================
{% if caching_enabled == 1 or caching_enabled == true -%}
# Asset Caching
include conf.d/include/assets.conf;
{% endif %}
================================================
FILE: backend/templates/_certificates.conf
================================================
{% if certificate and certificate_id > 0 -%}
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
{% else %}
# Custom SSL
ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
{% endif %}
{% endif %}
================================================
FILE: backend/templates/_certificates_stream.conf
================================================
{% if certificate and certificate_id > 0 %}
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/ssl-cache-stream.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
{%- else %}
# Custom SSL
ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
{%- endif -%}
{%- endif -%}
================================================
FILE: backend/templates/_exploits.conf
================================================
{% if block_exploits == 1 or block_exploits == true %}
# Block Exploits
include conf.d/include/block-exploits.conf;
{% endif %}
================================================
FILE: backend/templates/_forced_ssl.conf
================================================
{% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %}
# Force SSL
{% if trust_forwarded_proto == true %}
set $trust_forwarded_proto "T";
{% else %}
set $trust_forwarded_proto "F";
{% endif %}
include conf.d/include/force-ssl.conf;
{% endif %}
{% endif %}
================================================
FILE: backend/templates/_header_comment.conf
================================================
# ------------------------------------------------------------
# {{ domain_names | join: ", " }}
# ------------------------------------------------------------
================================================
FILE: backend/templates/_hsts.conf
================================================
{% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %}
{% if hsts_enabled == 1 or hsts_enabled == true %}
# HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)
add_header Strict-Transport-Security $hsts_header always;
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: backend/templates/_hsts_map.conf
================================================
map $scheme $hsts_header {
https "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload";
}
================================================
FILE: backend/templates/_listen.conf
================================================
listen 80;
{% if ipv6 -%}
listen [::]:80;
{% else -%}
#listen [::]:80;
{% endif %}
{% if certificate -%}
listen 443 ssl;
{% if ipv6 -%}
listen [::]:443 ssl;
{% else -%}
#listen [::]:443;
{% endif %}
{% endif %}
server_name {{ domain_names | join: " " }};
{% if http2_support == 1 or http2_support == true %}
http2 on;
{% else -%}
http2 off;
{% endif %}
================================================
FILE: backend/templates/_location.conf
================================================
location {{ path }} {
{{ advanced_config }}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
{% include "_access.conf" %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
{% endif %}
}
================================================
FILE: backend/templates/dead_host.conf
================================================
{% include "_header_comment.conf" %}
{% if enabled %}
{% include "_hsts_map.conf" %}
server {
{% include "_listen.conf" %}
{% include "_certificates.conf" %}
{% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
access_log /data/logs/dead-host-{{ id }}_access.log standard;
error_log /data/logs/dead-host-{{ id }}_error.log warn;
{{ advanced_config }}
{% if use_default_location %}
location / {
{% include "_hsts.conf" %}
return 404;
}
{% endif %}
# Custom
include /data/nginx/custom/server_dead[.]conf;
}
{% endif %}
================================================
FILE: backend/templates/default.conf
================================================
# ------------------------------------------------------------
# Default Site
# ------------------------------------------------------------
{% if value == "congratulations" %}
# Skipping output, congratulations page configration is baked in.
{%- else %}
server {
listen 80 default;
{% if ipv6 -%}
listen [::]:80 default;
{% else -%}
#listen [::]:80 default;
{% endif %}
server_name default-host.localhost;
access_log /data/logs/default-host_access.log combined;
error_log /data/logs/default-host_error.log warn;
{% include "_exploits.conf" %}
include conf.d/include/letsencrypt-acme-challenge.conf;
{%- if value == "404" %}
location / {
return 404;
}
{% endif %}
{%- if value == "444" %}
location / {
return 444;
}
{% endif %}
{%- if value == "redirect" %}
location / {
return 301 {{ meta.redirect }};
}
{%- endif %}
{%- if value == "html" %}
root /data/nginx/default_www;
location / {
try_files $uri /index.html;
}
{%- endif %}
}
{% endif %}
================================================
FILE: backend/templates/ip_ranges.conf
================================================
{% for range in ip_ranges %}
set_real_ip_from {{ range }};
{% endfor %}
================================================
FILE: backend/templates/letsencrypt-request.conf
================================================
{% include "_header_comment.conf" %}
server {
listen 80;
{% if ipv6 -%}
listen [::]:80;
{% endif %}
server_name {{ domain_names | join: " " }};
access_log /data/logs/letsencrypt-requests_access.log standard;
error_log /data/logs/letsencrypt-requests_error.log warn;
include conf.d/include/letsencrypt-acme-challenge.conf;
location / {
return 404;
}
}
================================================
FILE: backend/templates/proxy_host.conf
================================================
{% include "_header_comment.conf" %}
{% if enabled %}
{% include "_hsts_map.conf" %}
server {
set $forward_scheme {{ forward_scheme }};
set $server "{{ forward_host }}";
set $port {{ forward_port }};
{% include "_listen.conf" %}
{% include "_certificates.conf" %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
{% endif %}
access_log /data/logs/proxy-host-{{ id }}_access.log proxy;
error_log /data/logs/proxy-host-{{ id }}_error.log warn;
{{ advanced_config }}
{{ locations }}
{% if use_default_location %}
location / {
{% include "_access.conf" %}
{% include "_hsts.conf" %}
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
{% endif %}
# Proxy!
include conf.d/include/proxy.conf;
}
{% endif %}
# Custom
include /data/nginx/custom/server_proxy[.]conf;
}
{% endif %}
================================================
FILE: backend/templates/redirection_host.conf
================================================
{% include "_header_comment.conf" %}
{% if enabled %}
{% include "_hsts_map.conf" %}
server {
{% include "_listen.conf" %}
{% include "_certificates.conf" %}
{% include "_assets.conf" %}
{% include "_exploits.conf" %}
{% include "_hsts.conf" %}
{% include "_forced_ssl.conf" %}
access_log /data/logs/redirection-host-{{ id }}_access.log standard;
error_log /data/logs/redirection-host-{{ id }}_error.log warn;
{{ advanced_config }}
{% if use_default_location %}
location / {
{% include "_hsts.conf" %}
{% if preserve_path == 1 or preserve_path == true %}
return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri;
{% else %}
return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }};
{% endif %}
}
{% endif %}
# Custom
include /data/nginx/custom/server_redirect[.]conf;
}
{% endif %}
================================================
FILE: backend/templates/stream.conf
================================================
# ------------------------------------------------------------
# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }}
# ------------------------------------------------------------
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{%- include "_certificates_stream.conf" %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
access_log /data/logs/stream-{{ id }}_access.log stream;
error_log /data/logs/stream-{{ id }}_error.log warn;
# Custom
include /data/nginx/custom/server_stream[.]conf;
include /data/nginx/custom/server_stream_tcp[.]conf;
}
{% endif %}
{% if udp_forwarding == 1 or udp_forwarding == true -%}
server {
listen {{ incoming_port }} udp;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
access_log /data/logs/stream-{{ id }}_access.log stream;
error_log /data/logs/stream-{{ id }}_error.log warn;
# Custom
include /data/nginx/custom/server_stream[.]conf;
include /data/nginx/custom/server_stream_udp[.]conf;
}
{% endif %}
{% endif %}
================================================
FILE: backend/validate-schema.js
================================================
#!/usr/bin/node
import SwaggerParser from "@apidevtools/swagger-parser";
import chalk from "chalk";
import { getCompiledSchema } from "./schema/index.js";
const log = console.log;
getCompiledSchema().then(async (swaggerJSON) => {
try {
const api = await SwaggerParser.validate(swaggerJSON);
console.log("API name: %s, Version: %s", api.info.title, api.info.version);
log(chalk.green("❯ Schema is valid"));
} catch (e) {
console.error(e);
log(chalk.red("❯", e.message), "\n");
process.exit(1);
}
});
================================================
FILE: docker/.dive-ci
================================================
rules:
# If the efficiency is measured below X%, mark as failed.
# Expressed as a ratio between 0-1.
lowestEfficiency: 0.99
# If the amount of wasted space is at least X or larger than X, mark as failed.
# Expressed in B, KB, MB, and GB.
highestWastedBytes: 15MB
# If the amount of wasted space makes up for X% or more of the image, mark as failed.
# Note: the base image layer is NOT included in the total image size.
# Expressed as a ratio between 0-1; fails if the threshold is met or crossed.
highestUserWastedPercent: 0.02
================================================
FILE: docker/Dockerfile
================================================
# This is a Dockerfile intended to be built using `docker buildx`
# for multi-arch support. Building with `docker build` may have unexpected results.
# This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/testca AS testca
FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM
ARG BUILD_VERSION
ARG BUILD_COMMIT
ARG BUILD_DATE
# See: https://github.com/just-containers/s6-overlay/blob/master/README.md
ENV SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_FIX_ATTRS_HIDDEN=1 \
S6_KILL_FINISH_MAXTIME=10000 \
S6_VERBOSITY=1 \
NODE_ENV=production \
NPM_BUILD_VERSION="${BUILD_VERSION}" \
NPM_BUILD_COMMIT="${BUILD_COMMIT}" \
NPM_BUILD_DATE="${BUILD_DATE}" \
NODE_OPTIONS="--openssl-legacy-provider"
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \
&& apt-get install -y --no-install-recommends jq logrotate \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# s6 overlay
COPY docker/scripts/install-s6 /tmp/install-s6
RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
EXPOSE 80 81 443
COPY backend /app
COPY frontend/dist /app/frontend
WORKDIR /app
RUN yarn install \
&& yarn cache clean
# add late to limit cache-busting by modifications
COPY docker/rootfs /
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
# Remove frontend service not required for prod, dev nginx config as well
RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager
VOLUME [ "/data" ]
ENTRYPOINT [ "/init" ]
LABEL org.label-schema.schema-version="1.0" \
org.label-schema.license="MIT" \
org.label-schema.name="nginx-proxy-manager" \
org.label-schema.description="Docker container for managing Nginx proxy hosts with a simple, powerful interface " \
org.label-schema.url="https://github.com/jc21/nginx-proxy-manager" \
org.label-schema.vcs-url="https://github.com/jc21/nginx-proxy-manager.git" \
org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:latest"
================================================
FILE: docker/ci.env
================================================
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=pgdb.internal
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
AUTHENTIK_BOOTSTRAP_PASSWORD=admin
AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com
================================================
FILE: docker/dev/Dockerfile
================================================
FROM nginxproxymanager/testca AS testca
FROM nginxproxymanager/nginx-full:certbot-node
LABEL maintainer="Jamie Curnow "
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_FIX_ATTRS_HIDDEN=1 \
S6_KILL_FINISH_MAXTIME=10000 \
S6_VERBOSITY=2 \
NODE_OPTIONS="--openssl-legacy-provider"
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \
&& apt-get install -y jq python3-pip logrotate moreutils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Task
WORKDIR /usr
RUN curl -sL https://taskfile.dev/install.sh | sh
WORKDIR /root
COPY rootfs /
COPY scripts/install-s6 /tmp/install-s6
RUN rm -f /etc/nginx/conf.d/production.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \
&& /tmp/install-s6 "${TARGETPLATFORM}" \
&& rm -f /tmp/install-s6 \
&& chmod 644 -R /root/.cache
# Certs for testing purposes
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443
ENTRYPOINT [ "/init" ]
================================================
FILE: docker/dev/dnsrouter-config.json
================================================
{
"log": {
"format": "nice",
"level": "debug"
},
"servers": [
{
"host": "0.0.0.0",
"port": 53,
"upstreams": [
{
"regex": "website[0-9]+.example\\.com",
"upstream": "127.0.0.11"
},
{
"regex": ".*\\.example\\.com",
"upstream": "1.1.1.1"
},
{
"regex": "local",
"nxdomain": true
}
],
"internal": null,
"default_upstream": "127.0.0.11"
}
]
}
================================================
FILE: docker/dev/letsencrypt.ini
================================================
text = True
non-interactive = True
webroot-path = /data/letsencrypt-acme-challenge
key-type = ecdsa
elliptic-curve = secp384r1
preferred-chain = ISRG Root X1
server =
================================================
FILE: docker/dev/pdns-db.sql
================================================
/*
How this was generated:
1. bring up an empty pdns stack
2. use api to create a zone ...
curl -X POST \
'http://npm.dev:8081/api/v1/servers/localhost/zones' \
--header 'X-API-Key: npm' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "example.com.",
"kind": "Native",
"masters": [],
"nameservers": [
"ns1.pdns.",
"ns2.pdns."
]
}'
3. Dump sql:
docker exec -ti npm.pdns.db mysqldump -u pdns -p pdns
*/
----------------------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `comments`
--
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`type` varchar(10) NOT NULL,
`modified_at` int(11) NOT NULL,
`account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,
`comment` text CHARACTER SET utf8mb3 NOT NULL,
PRIMARY KEY (`id`),
KEY `comments_name_type_idx` (`name`,`type`),
KEY `comments_order_idx` (`domain_id`,`modified_at`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `comments`
--
LOCK TABLES `comments` WRITE;
/*!40000 ALTER TABLE `comments` DISABLE KEYS */;
/*!40000 ALTER TABLE `comments` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `cryptokeys`
--
DROP TABLE IF EXISTS `cryptokeys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `cryptokeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`flags` int(11) NOT NULL,
`active` tinyint(1) DEFAULT NULL,
`published` tinyint(1) DEFAULT 1,
`content` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `domainidindex` (`domain_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `cryptokeys`
--
LOCK TABLES `cryptokeys` WRITE;
/*!40000 ALTER TABLE `cryptokeys` DISABLE KEYS */;
/*!40000 ALTER TABLE `cryptokeys` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `domainmetadata`
--
DROP TABLE IF EXISTS `domainmetadata`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `domainmetadata` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`kind` varchar(32) DEFAULT NULL,
`content` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `domainmetadata_idx` (`domain_id`,`kind`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `domainmetadata`
--
LOCK TABLES `domainmetadata` WRITE;
/*!40000 ALTER TABLE `domainmetadata` DISABLE KEYS */;
INSERT INTO `domainmetadata` VALUES
(1,1,'SOA-EDIT-API','DEFAULT');
/*!40000 ALTER TABLE `domainmetadata` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `domains`
--
DROP TABLE IF EXISTS `domains`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `domains` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`master` varchar(128) DEFAULT NULL,
`last_check` int(11) DEFAULT NULL,
`type` varchar(8) NOT NULL,
`notified_serial` int(10) unsigned DEFAULT NULL,
`account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,
`options` varchar(64000) DEFAULT NULL,
`catalog` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name_index` (`name`),
KEY `catalog_idx` (`catalog`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `domains`
--
LOCK TABLES `domains` WRITE;
/*!40000 ALTER TABLE `domains` DISABLE KEYS */;
INSERT INTO `domains` VALUES
(1,'example.com','',NULL,'NATIVE',NULL,'',NULL,NULL);
/*!40000 ALTER TABLE `domains` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `records`
--
DROP TABLE IF EXISTS `records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `records` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`type` varchar(10) DEFAULT NULL,
`content` varchar(64000) DEFAULT NULL,
`ttl` int(11) DEFAULT NULL,
`prio` int(11) DEFAULT NULL,
`disabled` tinyint(1) DEFAULT 0,
`ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
`auth` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`),
KEY `nametype_index` (`name`,`type`),
KEY `domain_id` (`domain_id`),
KEY `ordername` (`ordername`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `records`
--
LOCK TABLES `records` WRITE;
/*!40000 ALTER TABLE `records` DISABLE KEYS */;
INSERT INTO `records` VALUES
(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1),
(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1),
(3,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2023030501 10800 3600 604800 3600',1500,0,0,NULL,1);
/*!40000 ALTER TABLE `records` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `supermasters`
--
DROP TABLE IF EXISTS `supermasters`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `supermasters` (
`ip` varchar(64) NOT NULL,
`nameserver` varchar(255) NOT NULL,
`account` varchar(40) CHARACTER SET utf8mb3 NOT NULL,
PRIMARY KEY (`ip`,`nameserver`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `supermasters`
--
LOCK TABLES `supermasters` WRITE;
/*!40000 ALTER TABLE `supermasters` DISABLE KEYS */;
/*!40000 ALTER TABLE `supermasters` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `tsigkeys`
--
DROP TABLE IF EXISTS `tsigkeys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tsigkeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`algorithm` varchar(50) DEFAULT NULL,
`secret` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `namealgoindex` (`name`,`algorithm`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `tsigkeys`
--
LOCK TABLES `tsigkeys` WRITE;
/*!40000 ALTER TABLE `tsigkeys` DISABLE KEYS */;
/*!40000 ALTER TABLE `tsigkeys` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
================================================
FILE: docker/dev/squid.conf
================================================
# WELCOME TO SQUID 6.6
# ----------------------------
#
# This is the documentation for the Squid configuration file.
# This documentation can also be found online at:
# http://www.squid-cache.org/Doc/config/
#
# You may wish to look at the Squid home page and wiki for the
# FAQ and other documentation:
# http://www.squid-cache.org/
# https://wiki.squid-cache.org/SquidFaq
# https://wiki.squid-cache.org/ConfigExamples
#
# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.0.0.0/8
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 81
acl Safe_ports port 443 # https
#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports
# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports
# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager
# This default configuration only allows localhost requests because a more
# permissive Squid installation could introduce new attack vectors into the
# network by proxying external TCP connections to unprotected services.
http_access allow localhost
# The two deny rules below are unnecessary in this default configuration
# because they are followed by a "deny all" rule. However, they may become
# critically important when you start allowing external requests below them.
# Protect web applications running on the same server as Squid. They often
# assume that only local users can access them at "localhost" ports.
http_access deny to_localhost
# Protect cloud servers that provide local users with sensitive info about
# their server via certain well-known link-local (a.k.a. APIPA) addresses.
http_access deny to_linklocal
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#
include /etc/squid/conf.d/*.conf
# For example, to allow access from your local networks, you may uncomment the
# following rule (and/or add rules that match your definition of "local"):
# http_access allow localnet
# And finally deny all other access to this proxy
http_access deny all
# Squid normally listens to port 3128
http_port 3128
# Leave coredumps in the first cache dir
coredump_dir /var/spool/squid
#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
# example pattern for deb packages
#refresh_pattern (\.deb|\.udeb)$ 129600 100% 129600
refresh_pattern . 0 20% 4320
================================================
FILE: docker/docker-compose.ci.mysql.yml
================================================
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
fullstack:
environment:
DB_MYSQL_HOST: 'db-mysql'
DB_MYSQL_PORT: '3306'
DB_MYSQL_USER: 'npm'
DB_MYSQL_PASSWORD: 'npmpass'
DB_MYSQL_NAME: 'npm'
depends_on:
- db-mysql
db-mysql:
image: jc21/mariadb-aria
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npmpass'
MARIADB_AUTO_UPGRADE: '1'
volumes:
- mysql_vol:/var/lib/mysql
networks:
- fulltest
volumes:
mysql_vol:
================================================
FILE: docker/docker-compose.ci.postgres.yml
================================================
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
cypress:
environment:
CYPRESS_stack: "postgres"
fullstack:
environment:
DB_POSTGRES_HOST: "pgdb.internal"
DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass"
DB_POSTGRES_NAME: "npm"
depends_on:
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
db-postgres:
image: postgres:17
environment:
POSTGRES_USER: "npm"
POSTGRES_PASSWORD: "npmpass"
POSTGRES_DB: "npm"
volumes:
- psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
networks:
fulltest:
aliases:
- pgdb.internal
authentik-redis:
image: "redis:alpine"
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis_vol:/data
networks:
- fulltest
authentik:
image: ghcr.io/goauthentik/server:2024.10.1
restart: unless-stopped
command: server
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
networks:
- fulltest
authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1
restart: unless-stopped
command: worker
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
networks:
- fulltest
authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1
environment:
AUTHENTIK_HOST: "http://authentik:9000"
AUTHENTIK_INSECURE: "true"
AUTHENTIK_TOKEN: "wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp"
restart: unless-stopped
depends_on:
- authentik
networks:
- fulltest
volumes:
psql_vol:
redis_vol:
================================================
FILE: docker/docker-compose.ci.sqlite.yml
================================================
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
fullstack:
environment:
DB_SQLITE_FILE: '/data/mydb.sqlite'
PUID: 1000
PGID: 1000
DISABLE_IPV6: 'true'
================================================
FILE: docker/docker-compose.ci.yml
================================================
# WARNING: This is a CI docker-compose file used for building
# and testing of the entire app, it should not be used for production.
# This is a base compose file, it should be extended with a
# docker-compose.ci.*.yml file
services:
fullstack:
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
environment:
TZ: "${TZ:-Australia/Brisbane}"
DEBUG: "true"
CI: "true"
FORCE_COLOR: 1
# Required for DNS Certificate provisioning in CI
LE_SERVER: "https://ca.internal/acme/acme/directory"
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt"
volumes:
- "npm_data_ci:/data"
- "npm_le_ci:/etc/letsencrypt"
- "./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
expose:
- "80/tcp"
- "81/tcp"
- "443/tcp"
- "1500/tcp"
- "1501/tcp"
- "1502/tcp"
- "1503/tcp"
networks:
fulltest:
aliases:
- website1.example.com
- website2.example.com
- website3.example.com
stepca:
image: jc21/testca
volumes:
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
fulltest:
aliases:
- ca.internal
pdns:
image: pschiffe/pdns-mysql:4.8
volumes:
- "/etc/localtime:/etc/localtime:ro"
environment:
PDNS_master: "yes"
PDNS_api: "yes"
PDNS_api_key: "npm"
PDNS_webserver: "yes"
PDNS_webserver_address: "0.0.0.0"
PDNS_webserver_password: "npm"
PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_version_string: "anonymous"
PDNS_default_ttl: 1500
PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_gmysql_host: pdns-db
PDNS_gmysql_port: 3306
PDNS_gmysql_user: pdns
PDNS_gmysql_password: pdns
PDNS_gmysql_dbname: pdns
depends_on:
- pdns-db
networks:
fulltest:
aliases:
- ns1.pdns
- ns2.pdns
pdns-db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: "pdns"
MYSQL_DATABASE: "pdns"
MYSQL_USER: "pdns"
MYSQL_PASSWORD: "pdns"
volumes:
- "pdns_mysql_vol:/var/lib/mysql"
- "/etc/localtime:/etc/localtime:ro"
- "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro"
networks:
- fulltest
dnsrouter:
image: jc21/dnsrouter
volumes:
- ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
networks:
- fulltest
cypress:
image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}"
build:
context: ../
dockerfile: test/cypress/Dockerfile
environment:
HTTP_PROXY: "squid:3128"
HTTPS_PROXY: "squid:3128"
volumes:
- "cypress_logs:/test/results"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs
networks:
- fulltest
squid:
image: ubuntu/squid
volumes:
- "./dev/squid.conf:/etc/squid/squid.conf:ro"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
- fulltest
volumes:
cypress_logs:
npm_data_ci:
npm_le_ci:
pdns_mysql_vol:
networks:
fulltest:
name: "npm-${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
================================================
FILE: docker/docker-compose.dev.yml
================================================
# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
services:
fullstack:
image: npm2dev:core
container_name: npm2dev.core
build:
context: ./
dockerfile: ./dev/Dockerfile
ports:
- 3080:80
- 3081:81
- 3443:443
networks:
nginx_proxy_manager:
aliases:
- website1.example.com
- website2.example.com
- website3.example.com
environment:
TZ: "${TZ:-Australia/Brisbane}"
PUID: 1000
PGID: 1000
FORCE_COLOR: 1
# specifically for dev:
DEBUG: "true"
DEVELOPMENT: "true"
LE_STAGING: "true"
# db:
# DB_MYSQL_HOST: 'db'
# DB_MYSQL_PORT: '3306'
# DB_MYSQL_USER: 'npm'
# DB_MYSQL_PASSWORD: 'npm'
# DB_MYSQL_NAME: 'npm'
# db-postgres:
DB_POSTGRES_HOST: "pgdb.internal"
DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass"
DB_POSTGRES_NAME: "npm"
# DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true"
# Required for DNS Certificate provisioning testing:
LE_SERVER: "https://ca.internal/acme/acme/directory"
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt"
volumes:
- npm_data:/data
- le_data:/etc/letsencrypt
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- ../backend:/app
- ../frontend:/frontend
- "/etc/localtime:/etc/localtime:ro"
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
depends_on:
- db
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
working_dir: /app
db:
image: jc21/mariadb-aria
container_name: npm2dev.db
ports:
- 33306:3306
networks:
- nginx_proxy_manager
environment:
TZ: "${TZ:-Australia/Brisbane}"
MYSQL_ROOT_PASSWORD: "npm"
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
MYSQL_PASSWORD: "npm"
volumes:
- db_data:/var/lib/mysql
- "/etc/localtime:/etc/localtime:ro"
db-postgres:
image: postgres:17
container_name: npm2dev.db-postgres
environment:
POSTGRES_USER: "npm"
POSTGRES_PASSWORD: "npmpass"
POSTGRES_DB: "npm"
volumes:
- psql_data:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
networks:
nginx_proxy_manager:
aliases:
- pgdb.internal
stepca:
image: jc21/testca
container_name: npm2dev.stepca
volumes:
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
nginx_proxy_manager:
aliases:
- ca.internal
dnsrouter:
image: jc21/dnsrouter
container_name: npm2dev.dnsrouter
volumes:
- ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
networks:
- nginx_proxy_manager
swagger:
image: swaggerapi/swagger-ui:latest
container_name: npm2dev.swagger
ports:
- 3082:80
environment:
URL: "http://npm:81/api/schema"
PORT: "80"
depends_on:
- fullstack
squid:
image: ubuntu/squid
container_name: npm2dev.squid
volumes:
- "./dev/squid.conf:/etc/squid/squid.conf:ro"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
- nginx_proxy_manager
ports:
- 8128:3128
pdns:
image: pschiffe/pdns-mysql:4.8
container_name: npm2dev.pdns
volumes:
- "/etc/localtime:/etc/localtime:ro"
environment:
PDNS_master: "yes"
PDNS_api: "yes"
PDNS_api_key: "npm"
PDNS_webserver: "yes"
PDNS_webserver_address: "0.0.0.0"
PDNS_webserver_password: "npm"
PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_version_string: "anonymous"
PDNS_default_ttl: 1500
PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_gmysql_host: pdns-db
PDNS_gmysql_port: 3306
PDNS_gmysql_user: pdns
PDNS_gmysql_password: pdns
PDNS_gmysql_dbname: pdns
depends_on:
- pdns-db
networks:
nginx_proxy_manager:
aliases:
- ns1.pdns
- ns2.pdns
pdns-db:
image: mariadb
container_name: npm2dev.pdns-db
environment:
MYSQL_ROOT_PASSWORD: "pdns"
MYSQL_DATABASE: "pdns"
MYSQL_USER: "pdns"
MYSQL_PASSWORD: "pdns"
volumes:
- "pdns_mysql:/var/lib/mysql"
- "/etc/localtime:/etc/localtime:ro"
- "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro"
networks:
- nginx_proxy_manager
cypress:
image: npm2dev:cypress
container_name: npm2dev.cypress
build:
context: ../
dockerfile: test/cypress/Dockerfile
environment:
HTTP_PROXY: "squid:3128"
HTTPS_PROXY: "squid:3128"
volumes:
- "../test/results:/results"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs
networks:
- nginx_proxy_manager
authentik-redis:
image: "redis:alpine"
container_name: npm2dev.authentik-redis
command: --save 60 1 --loglevel warning
networks:
- nginx_proxy_manager
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis_data:/data
authentik:
image: ghcr.io/goauthentik/server:2024.10.1
container_name: npm2dev.authentik
restart: unless-stopped
command: server
networks:
- nginx_proxy_manager
env_file:
- ci.env
ports:
- 9000:9000
depends_on:
- authentik-redis
- db-postgres
authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1
container_name: npm2dev.authentik-worker
restart: unless-stopped
command: worker
networks:
- nginx_proxy_manager
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1
container_name: npm2dev.authentik-ldap
networks:
- nginx_proxy_manager
environment:
AUTHENTIK_HOST: "http://authentik:9000"
AUTHENTIK_INSECURE: "true"
AUTHENTIK_TOKEN: "wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp"
restart: unless-stopped
depends_on:
- authentik
volumes:
npm_data:
name: npm2dev_core_data
le_data:
name: npm2dev_le_data
db_data:
name: npm2dev_db_data
pdns_mysql:
name: npnpm2dev_pdns_mysql
psql_data:
name: npm2dev_psql_data
redis_data:
name: npm2dev_redis_data
networks:
nginx_proxy_manager:
name: npm2dev_network
================================================
FILE: docker/rootfs/etc/letsencrypt.ini
================================================
text = True
non-interactive = True
webroot-path = /data/letsencrypt-acme-challenge
key-type = ecdsa
elliptic-curve = secp384r1
preferred-chain = ISRG Root X1
================================================
FILE: docker/rootfs/etc/logrotate.d/nginx-proxy-manager
================================================
/data/logs/*_access.log /data/logs/*/access.log {
su npm npm
create 0644
weekly
rotate 4
missingok
notifempty
compress
sharedscripts
postrotate
kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}
/data/logs/*_error.log /data/logs/*/error.log {
su npm npm
create 0644
weekly
rotate 10
missingok
notifempty
compress
sharedscripts
postrotate
kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/default.conf
================================================
# "You are not configured" page, which is the default if another default doesn't exist
server {
listen 80;
listen [::]:80;
set $forward_scheme "http";
set $server "127.0.0.1";
set $port "80";
server_name localhost-nginx-proxy-manager;
access_log /data/logs/fallback_http_access.log standard;
error_log /data/logs/fallback_http_error.log warn;
include conf.d/include/assets.conf;
include conf.d/include/block-exploits.conf;
include conf.d/include/letsencrypt-acme-challenge.conf;
location / {
index index.html;
root /var/www/html;
}
}
# First 443 Host, which is the default if another default doesn't exist
server {
listen 443 ssl;
listen [::]:443 ssl;
set $forward_scheme "https";
set $server "127.0.0.1";
set $port "443";
server_name localhost;
access_log /data/logs/fallback_http_access.log standard;
error_log /dev/null crit;
include conf.d/include/ssl-ciphers.conf;
ssl_reject_handshake on;
return 444;
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/dev.conf
================================================
server {
listen 81 default;
listen [::]:81 default;
server_name nginxproxymanager-dev;
root /app/frontend/dist;
access_log /dev/null;
location /api {
return 302 /api/;
}
location /api/ {
add_header X-Served-By $host;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:3000/;
proxy_read_timeout 15m;
proxy_send_timeout 15m;
}
location / {
add_header X-Served-By $host;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:5173;
}
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/.gitignore
================================================
resolvers.conf
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/assets.conf
================================================
location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ {
if_modified_since off;
# use the public cache
proxy_cache public-cache;
proxy_cache_key $host$request_uri;
# ignore these headers for media
proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires;
# cache 200s and also 404s (not ideal but there are a few 404 images for some reason)
proxy_cache_valid any 30m;
proxy_cache_valid 404 1m;
# strip this header to avoid If-Modified-Since requests
proxy_hide_header Last-Modified;
proxy_hide_header Cache-Control;
proxy_hide_header Vary;
proxy_cache_bypass 0;
proxy_no_cache 0;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404;
proxy_connect_timeout 5s;
proxy_read_timeout 45s;
expires @30m;
access_log off;
include conf.d/include/proxy.conf;
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf
================================================
## Block SQL injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\(") {
set $block_sql_injections 1;
}
if ($query_string ~ "union.*all.*select.*") {
set $block_sql_injections 1;
}
if ($query_string ~ "concat.*\(") {
set $block_sql_injections 1;
}
if ($block_sql_injections = 1) {
return 403;
}
## Block file injections
set $block_file_injections 0;
if ($query_string ~ "[a-zA-Z0-9_]=http://") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
set $block_file_injections 1;
}
if ($block_file_injections = 1) {
return 403;
}
## Block common exploits
set $block_common_exploits 0;
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") {
set $block_common_exploits 1;
}
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "proc/self/environ") {
set $block_common_exploits 1;
}
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
set $block_common_exploits 1;
}
if ($query_string ~ "base64_(en|de)code\(.*\)") {
set $block_common_exploits 1;
}
if ($block_common_exploits = 1) {
return 403;
}
## Block spam
set $block_spam 0;
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") {
set $block_spam 1;
}
if ($block_spam = 1) {
return 403;
}
## Block user agents
set $block_user_agents 0;
# Disable Akeeba Remote Control 2.5 and earlier
if ($http_user_agent ~ "Indy Library") {
set $block_user_agents 1;
}
# Common bandwidth hoggers and hacking tools.
if ($http_user_agent ~ "libwww-perl") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetRight") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetWeb!") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go!Zilla") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Download Demon") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go-Ahead-Got-It") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "TurnitinBot") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GrabNet") {
set $block_user_agents 1;
}
if ($block_user_agents = 1) {
return 403;
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf
================================================
set $test "";
if ($scheme = "http") {
set $test "H";
}
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
set $test "${test}T";
}
# Check if the ssl staff has been handled
set $test_ssl_handled "";
if ($trust_forwarded_proto = "") {
set $trust_forwarded_proto "F";
}
if ($trust_forwarded_proto = "T") {
set $test_ssl_handled "${test_ssl_handled}T";
}
if ($http_x_forwarded_proto = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($http_x_forwarded_scheme = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($test_ssl_handled = "TSS") {
set $test_ssl_handled "TS";
}
if ($test_ssl_handled = "TS") {
set $test "${test}S";
}
if ($test = H) {
return 301 https://$host$request_uri;
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf
================================================
# This should be left blank is it is populated programatically
# by the application backend.
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf
================================================
# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel
# other regex checks, because in our other config files have regex rule that denies access to files with dotted names.
location ^~ /.well-known/acme-challenge/ {
# Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure
# we need to open up access by turning off auth and IP ACL for this location.
auth_basic off;
auth_request off;
allow all;
# Set correct content type. According to this:
# https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29
# Current specification requires "text/plain" or no content header at all.
# It seems that "text/plain" is a safe option.
default_type "text/plain";
# This directory must be the same as in /etc/letsencrypt/cli.ini
# as "webroot-path" parameter. Also don't forget to set "authenticator" parameter
# there to "webroot".
# Do NOT use alias, use root! Target directory is located here:
# /var/www/common/letsencrypt/.well-known/acme-challenge/
root /data/letsencrypt-acme-challenge;
}
# Hide /acme-challenge subdirectory and return 404 on all requests.
# It is somewhat more secure than letting Nginx return 403.
# Ending slash is important!
location = /.well-known/acme-challenge/ {
return 404;
}
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/log-proxy.conf
================================================
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"';
log_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"';
access_log /data/logs/fallback_http_access.log proxy;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/log-stream.conf
================================================
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';
access_log /data/logs/fallback_stream_access.log stream;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/proxy.conf
================================================
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $x_forwarded_scheme;
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port$request_uri;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf
================================================
ssl_session_timeout 5m;
ssl_session_cache shared:SSL_stream:50m;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
================================================
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
================================================
# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
================================================
FILE: docker/rootfs/etc/nginx/conf.d/production.conf
================================================
# Admin Interface
server {
listen 81 default;
listen [::]:81 default;
server_name nginxproxymanager;
root /app/frontend;
access_log /dev/null;
location /api {
return 302 /api/;
}
location /api/ {
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:3000/;
proxy_read_timeout 15m;
proxy_send_timeout 15m;
}
location / {
index index.html;
if ($request_uri ~ ^/(.*)\.html$) {
return 302 /$1;
}
try_files $uri $uri.html $uri/ /index.html;
}
}
================================================
FILE: docker/rootfs/etc/nginx/mime.types
================================================
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}
================================================
FILE: docker/rootfs/etc/nginx/nginx.conf
================================================
# run nginx in foreground
daemon off;
pid /run/nginx/nginx.pid;
user npm;
# Set number of worker processes automatically based on number of CPU cores.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
error_log /data/logs/fallback_error.log warn;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
# Custom
include /data/nginx/custom/root_top[.]conf;
events {
include /data/nginx/custom/events[.]conf;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server_tokens off;
tcp_nopush on;
tcp_nodelay on;
client_body_temp_path /tmp/nginx/body 1 2;
keepalive_timeout 90s;
proxy_connect_timeout 90s;
proxy_send_timeout 90s;
proxy_read_timeout 90s;
ssl_prefer_server_ciphers on;
gzip on;
proxy_ignore_client_abort off;
client_max_body_size 2000m;
server_names_hash_bucket_size 1024;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Accept-Encoding "";
proxy_cache off;
proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m;
proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-proxy[.]conf;
# Dynamically generated resolvers file
include /etc/nginx/conf.d/include/resolvers[.]conf;
# Default upstream scheme
map $host $forward_scheme {
default http;
}
# Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header
map $http_x_forwarded_proto $x_forwarded_proto {
"http" "http";
"https" "https";
default $scheme;
}
map $http_x_forwarded_scheme $x_forwarded_scheme {
"http" "http";
"https" "https";
default $scheme;
}
# Real IP Determination
# Local subnets:
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12; # Includes Docker subnet
set_real_ip_from 192.168.0.0/16;
# NPM generated CDN ip ranges:
include conf.d/include/ip_ranges[.]conf;
# always put the following 2 lines after ip subnets:
real_ip_header X-Real-IP;
real_ip_recursive on;
# Custom
include /data/nginx/custom/http_top[.]conf;
# Files generated by NPM
include /etc/nginx/conf.d/*.conf;
include /data/nginx/default_host/*.conf;
include /data/nginx/proxy_host/*.conf;
include /data/nginx/redirection_host/*.conf;
include /data/nginx/dead_host/*.conf;
include /data/nginx/temp/*.conf;
# Custom
include /data/nginx/custom/http[.]conf;
}
stream {
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-stream[.]conf;
# Files generated by NPM
include /data/nginx/stream/*.conf;
# Custom
include /data/nginx/custom/stream[.]conf;
}
# Custom
include /data/nginx/custom/root[.]conf;
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
cd /app || exit 1
log_info 'Starting backend ...'
if [ "${DEVELOPMENT:-}" = 'true' ]; then
s6-setuidgid "$PUID:$PGID" yarn install
exec 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"
else
while :
do
s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js"
sleep 1
done
fi
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type
================================================
longrun
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
# This service is DEVELOPMENT only.
if [ "$DEVELOPMENT" = 'true' ]; then
. /usr/bin/common.sh
cd /frontend || exit 1
HOME=$NPMHOME
export HOME
mkdir -p /frontend/dist
chown -R "$PUID:$PGID" /frontend/dist
log_info 'Starting frontend ...'
s6-setuidgid "$PUID:$PGID" yarn install
exec s6-setuidgid "$PUID:$PGID" yarn dev
else
exit 0
fi
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type
================================================
longrun
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
log_info 'Starting nginx ...'
exec s6-setuidgid "$PUID:$PGID" nginx
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type
================================================
longrun
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
. /usr/bin/common.sh
if [ "$(id -u)" != "0" ]; then
log_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."
fi
if [ "$DEBUG" = "true" ]; then
set -x
fi
. /etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh
. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info "Configuring $NPMUSER user ..."
if id -u "$NPMUSER" 2>/dev/null; then
# user already exists
usermod -u "$PUID" "$NPMUSER"
else
# Add user
useradd -o -u "$PUID" -U -d "$NPMHOME" -s /bin/false "$NPMUSER"
fi
log_info "Configuring $NPMGROUP group ..."
if [ "$(get_group_id "$NPMGROUP")" = '' ]; then
# Add group. This will not set the id properly if it's already taken
groupadd -f -g "$PGID" "$NPMGROUP"
else
groupmod -o -g "$PGID" "$NPMGROUP"
fi
# Set the group ID and check it
groupmod -o -g "$PGID" "$NPMGROUP"
if [ "$(get_group_id "$NPMGROUP")" != "$PGID" ]; then
echo "ERROR: Unable to set group id properly"
exit 1
fi
# Set the group against the user and check it
usermod -G "$PGID" "$NPMGROUP"
if [ "$(id -g "$NPMUSER")" != "$PGID" ] ; then
echo "ERROR: Unable to set group against the user properly"
exit 1
fi
# Home for user
mkdir -p "$NPMHOME"
chown -R "$PUID:$PGID" "$NPMHOME"
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Checking paths ...'
# Ensure /data is mounted
if [ ! -d '/data' ]; then
log_fatal '/data is not mounted! Check your docker configuration.'
fi
# Ensure /etc/letsencrypt is mounted
if [ ! -d '/etc/letsencrypt' ]; then
log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.'
fi
# Create required folders
mkdir -p \
/data/nginx \
/data/custom_ssl \
/data/logs \
/data/access \
/data/nginx/default_host \
/data/nginx/default_www \
/data/nginx/proxy_host \
/data/nginx/redirection_host \
/data/nginx/stream \
/data/nginx/dead_host \
/data/nginx/temp \
/data/letsencrypt-acme-challenge \
/run/nginx \
/tmp/nginx/body \
/var/log/nginx \
/var/lib/nginx/cache/public \
/var/lib/nginx/cache/private \
/var/cache/nginx/proxy_temp
touch /var/log/nginx/error.log || true
chmod 777 /var/log/nginx/error.log || true
chmod -R 777 /var/cache/nginx || true
chmod 644 /etc/logrotate.d/nginx-proxy-manager
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Setting ownership ...'
# root
chown root /tmp/nginx
locations=(
"/data"
"/etc/letsencrypt"
"/run/nginx"
"/tmp/nginx"
"/var/cache/nginx"
"/var/lib/logrotate"
"/var/lib/nginx"
"/var/log/nginx"
"/etc/nginx/nginx"
"/etc/nginx/nginx.conf"
"/etc/nginx/conf.d"
)
chownit() {
local dir="$1"
local recursive="${2:-true}"
local have
have="$(stat -c '%u:%g' "$dir")"
echo "- $dir ... "
if [ "$have" != "$PUID:$PGID" ]; then
if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then
chown -R "$PUID:$PGID" "$dir"
else
chown "$PUID:$PGID" "$dir"
fi
echo " DONE"
else
echo " SKIPPED"
fi
}
for loc in "${locations[@]}"; do
chownit "$loc"
done
if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then
log_info 'Skipping ownership change of certbot directories'
else
log_info 'Changing ownership of certbot directories, this may take some time ...'
chownit "/opt/certbot" false
chownit "/opt/certbot/bin" false
# Handle all site-packages directories efficiently
find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do
chownit "$SITE_PACKAGES_DIR"
done
fi
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
log_info 'Dynamic resolvers ...'
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm
if [ "$(is_true "${DISABLE_RESOLVER:-}")" = '0' ]; then
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
echo 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
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
fi
fi
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
# This command reads the `DISABLE_IPV6` env var and will either enable
# or disable ipv6 in all nginx configs based on this setting.
set -e
log_info 'IPv6 ...'
process_folder () {
FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX=
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
else
# IPV6 is enabled
echo "Enabling IPV6 in hosts in: $1"
SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g'
fi
for FILE in $FILES
do
echo "- ${FILE}"
TMPFILE="${FILE}.tmp"
if sed -E "$SED_REGEX" "$FILE" > "$TMPFILE" && [ -s "$TMPFILE" ]; then
mv "$TMPFILE" "$FILE"
else
echo "WARNING: skipping ${FILE} — sed produced empty output" >&2
rm -f "$TMPFILE"
fi
done
# ensure the files are still owned by the npm user
chown -R "$PUID:$PGID" "$1"
}
process_folder /etc/nginx/conf.d
process_folder /data/nginx
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
# in s6, environmental variables are written as text files for s6 to monitor
# search through full-path filenames for files ending in "__FILE"
log_info 'Docker secrets ...'
for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do
echo "[secret-init] Evaluating ${FILENAME##*/} ..."
# set SECRETFILE to the contents of the full-path textfile
SECRETFILE=$(cat "${FILENAME}")
# if SECRETFILE exists / is not null
if [[ -f "${SECRETFILE}" ]]; then
# strip the appended "__FILE" from environmental variable name ...
STRIPFILE=$(echo "${FILENAME}" | sed "s/__FILE//g")
# echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod!
# ... and set value to contents of secretfile
# since s6 uses text files, this is effectively "export ..."
printf $(cat "${SECRETFILE}") > "${STRIPFILE}"
# echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!"
echo "Success: ${STRIPFILE##*/} set from ${FILENAME##*/}"
else
echo "Cannot find secret in ${FILENAME}"
fi
done
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh
================================================
#!/command/with-contenv bash
# shellcheck shell=bash
set -e
set +x
echo "
-------------------------------------
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
-------------------------------------
User: $NPMUSER PUID:$PUID ID:$(id -u "$NPMUSER") GROUP:$(id -g "$NPMUSER")
Group: $NPMGROUP PGID:$PGID ID:$(get_group_id "$NPMGROUP")
-------------------------------------
"
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type
================================================
oneshot
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up
================================================
# shellcheck shell=bash
/etc/s6-overlay/s6-rc.d/prepare/00-all.sh
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx
================================================
================================================
FILE: docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare
================================================
================================================
FILE: docker/rootfs/root/.bashrc
================================================
#!/bin/bash
if [ -t 1 ]; then
export PS1="\e[1;34m[\e[1;33m\u@\e[1;32mdocker-\h\e[1;37m:\w\[\e[1;34m]\e[1;36m\\$ \e[0m"
fi
# Aliases
alias l='ls -lAsh --color'
alias ls='ls -C1 --color'
alias cp='cp -ip'
alias rm='rm -i'
alias mv='mv -i'
alias h='cd ~;clear;'
. /etc/os-release
echo -e -n '\E[1;34m'
figlet -w 120 "NginxProxyManager"
echo -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"
echo -e -n '\E[1;34m'
cat /built-for-arch
echo -e '\E[0m'
================================================
FILE: docker/rootfs/usr/bin/check-health
================================================
#!/bin/bash
OK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status')
if [ "$OK" == "OK" ]; then
echo "OK"
exit 0
else
echo "NOT OK"
exit 1
fi
================================================
FILE: docker/rootfs/usr/bin/common.sh
================================================
#!/bin/bash
set -e
CYAN='\E[1;36m'
BLUE='\E[1;34m'
YELLOW='\E[1;33m'
RED='\E[1;31m'
RESET='\E[0m'
export CYAN BLUE YELLOW RED RESET
PUID=${PUID:-0}
PGID=${PGID:-0}
# If changing the username and group name below,
# ensure all references to this user is also changed.
# See docker/rootfs/etc/logrotate.d/nginx-proxy-manager
# and docker/rootfs/etc/nginx/nginx.conf
NPMUSER=npm
NPMGROUP=npm
NPMHOME=/tmp/npmuserhome
export NPMUSER NPMGROUP NPMHOME
if [[ "$PUID" -ne '0' ]] && [ "$PGID" = '0' ]; then
# set group id to same as user id,
# the user probably forgot to specify the group id and
# it would be rediculous to intentionally use the root group
# for a non-root user
PGID=$PUID
fi
export PUID PGID
log_info () {
echo -e "${BLUE}❯ ${CYAN}$1${RESET}"
}
log_error () {
echo -e "${RED}❯ $1${RESET}"
}
# The `run` file will only execute 1 line so this helps keep things
# logically separated
log_fatal () {
echo -e "${RED}--------------------------------------${RESET}"
echo -e "${RED}ERROR: $1${RESET}"
echo -e "${RED}--------------------------------------${RESET}"
/run/s6/basedir/bin/halt
exit 1
}
# param $1: group_name
get_group_id () {
if [ "${1:-}" != '' ]; then
getent group "$1" | cut -d: -f3
fi
}
# param $1: value
is_true () {
VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')
if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then
echo '1'
else
echo '0'
fi
}
================================================
FILE: docker/rootfs/var/www/html/index.html
================================================
Default Site
Congratulations!
You've successfully started the Nginx Proxy Manager.
If you're seeing this site then you're trying to access a host that isn't set up yet.
Log in to the Admin panel to get started.
Powered by Nginx Proxy Manager
================================================
FILE: docker/scripts/install-s6
================================================
#!/bin/bash -e
# Note: This script is designed to be run inside a Docker Build for a container
CYAN='\E[1;36m'
YELLOW='\E[1;33m'
BLUE='\E[1;34m'
GREEN='\E[1;32m'
RESET='\E[0m'
S6_OVERLAY_VERSION=3.2.1.0
TARGETPLATFORM=${1:-linux/amd64}
# Determine the correct binary file for the architecture given
case $TARGETPLATFORM in
linux/arm64)
S6_ARCH=aarch64
;;
*)
S6_ARCH=x86_64
;;
esac
echo -e "${BLUE}❯ ${CYAN}Installing S6-overlay v${S6_OVERLAY_VERSION} for ${YELLOW}${TARGETPLATFORM} (${S6_ARCH})${RESET}"
curl -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"
curl -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"
tar -C / -Jxpf '/tmp/s6-overlay-noarch.tar.xz'
tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz"
rm -rf "/tmp/s6-overlay-${S6_ARCH}.tar.xz"
echo -e "${BLUE}❯ ${GREEN}S6-overlay install Complete${RESET}"
================================================
FILE: docs/.gitignore
================================================
dist
node_modules
ts
.temp
.cache
.vitepress/cache
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
*.gz
*.tgz
================================================
FILE: docs/.vitepress/config.mts
================================================
import { defineConfig, type DefaultTheme } from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Nginx Proxy Manager",
description: "Expose your services easily and securely",
head: [
["link", { rel: "icon", href: "/icon.png" }],
["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" }],
["meta", { property: "og:title", content: "Nginx Proxy Manager" }],
["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"}],
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }],
["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }],
["meta", { name: "twitter:card", content: "summary"}],
["meta", { name: "twitter:title", content: "Nginx Proxy Manager"}],
["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"}],
["meta", { name: "twitter:image", content: "https://nginxproxymanager.com/icon.png"}],
["meta", { name: "twitter:alt", content: "Nginx Proxy Manager"}],
// GA
['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}],
['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"],
],
sitemap: {
hostname: 'https://nginxproxymanager.com'
},
metaChunk: true,
srcDir: './src',
outDir: './dist',
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: { src: '/logo.svg', width: 24, height: 24 },
nav: [
{ text: 'Setup', link: '/setup/' },
],
sidebar: [
{
items: [
// { text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/' },
{ text: 'Screenshots', link: '/screenshots/' },
{ text: 'Setup Instructions', link: '/setup/' },
{ text: 'Advanced Configuration', link: '/advanced-config/' },
{ text: 'Upgrading', link: '/upgrading/' },
{ text: 'Frequently Asked Questions', link: '/faq/' },
{ text: 'Third Party', link: '/third-party/' },
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/NginxProxyManager/nginx-proxy-manager' }
],
search: {
provider: 'local'
},
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2016-present jc21.com'
}
}
});
================================================
FILE: docs/.vitepress/theme/custom.css
================================================
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #f15833 30%, #FAA42F);
--vp-home-hero-image-background-image: linear-gradient(-45deg, #aaaaaa 50%, #777777 50%);
--vp-home-hero-image-filter: blur(44px);
--vp-c-brand-1: #f15833;
--vp-c-brand-2: #FAA42F;
--vp-c-brand-3: #f15833;
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
.inline-img img {
display: inline;
margin-right: 8px;
}
================================================
FILE: docs/.vitepress/theme/index.ts
================================================
import DefaultTheme from 'vitepress/theme'
import './custom.css'
export default DefaultTheme
================================================
FILE: docs/package.json
================================================
{
"scripts": {
"dev": "vitepress dev --host",
"build": "vitepress build",
"preview": "vitepress preview",
"set-version": "./scripts/set-version.sh"
},
"devDependencies": {
"vitepress": "^1.6.4"
},
"dependencies": {}
}
================================================
FILE: docs/scripts/set-version.sh
================================================
#!/bin/bash
set -euf
# this script accepts a version number as an argument
# and replaces {{VERSION}} in src/*.md with the provided version number.
if [ "$#" -ne 1 ]; then
echo "Usage: $0 "
exit 1
fi
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR/.." || exit 1
VERSION="$1"
# find all .md files in src/ and replace {{VERSION}} with the provided version number
find src/ -type f -name "*.md" -exec sed -i "s/{{VERSION}}/$VERSION/g" {} \;
================================================
FILE: docs/src/advanced-config/index.md
================================================
---
outline: deep
---
# Advanced Configuration
## Running processes as a user/group
By default, the services (nginx etc) will run as `root` user inside the docker container.
You can change this behaviour by setting the following environment variables.
Not only will they run the services as this user/group, they will change the ownership
on the `data` and `letsencrypt` folders at startup.
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
environment:
PUID: 1000
PGID: 1000
# ...
```
This may have the side effect of a failed container start due to permission denied trying
to open port 80 on some systems. The only course to fix that is to remove the variables
and run as the default root user.
## Best Practice: Use a Docker network
For those who have a few of their upstream services running in Docker on the same Docker
host as NPM, here's a trick to secure things a bit better. By creating a custom Docker network,
you don't need to publish ports for your upstream services to all of the Docker host's interfaces.
Create a network, ie "scoobydoo":
```bash
docker network create scoobydoo
```
Then add the following to the `docker-compose.yml` file for both NPM and any other
services running on this Docker host:
```yml
networks:
default:
external: true
name: scoobydoo
```
Let's look at a Portainer example:
```yml
services:
portainer:
image: portainer/portainer
privileged: true
volumes:
- './data:/data'
- '/var/run/docker.sock:/var/run/docker.sock'
restart: unless-stopped
networks:
default:
external: true
name: scoobydoo
```
Now in the NPM UI you can create a proxy host with `portainer` as the hostname,
and port `9000` as the port. Even though this port isn't listed in the docker-compose
file, it's "exposed" by the Portainer Docker image for you and not available on
the Docker host outside of this Docker network. The service name is used as the
hostname, so make sure your service names are unique when using the same network.
## Docker Healthcheck
The `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this
feature by adding the following to the service in your `docker-compose.yml` file:
```yml
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
```
## Docker File Secrets
This image supports the use of Docker secrets to import from files and keep sensitive usernames or passwords from being passed or preserved in plaintext.
You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name.
```yml
secrets:
# Secrets are single-line text files where the sole content is the secret
# Paths in this example assume that secrets are kept in local folder called ".secrets"
DB_ROOT_PWD:
file: .secrets/db_root_pwd.txt
MYSQL_PWD:
file: .secrets/mysql_pwd.txt
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
ports:
# Public HTTP Port:
- '80:80'
# Public HTTPS Port:
- '443:443'
# Admin Web Port:
- '81:81'
environment:
# These are the settings to access your db
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
# DB_MYSQL_PASSWORD: "npm" # use secret instead
DB_MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
DB_MYSQL_NAME: "npm"
# If you would rather use Sqlite, remove all DB_MYSQL_* lines above
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
secrets:
- MYSQL_PWD
depends_on:
- db
db:
image: 'linuxserver/mariadb'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
TZ: 'Australia/Brisbane'
volumes:
- ./mariadb:/config
secrets:
- DB_ROOT_PWD
- MYSQL_PWD
```
## Disabling IPv6
On some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log:
> Address family not supported by protocol
The easy fix is to add a Docker environment variable to the Nginx Proxy Manager stack:
```yml
environment:
DISABLE_IPV6: 'true'
```
## Disabling IP Ranges Fetch
By 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:
```yml
environment:
IP_RANGES_FETCH_ENABLED: 'false'
```
## Custom Nginx Configurations
If you are a more advanced user, you might be itching for extra Nginx customizability.
NPM has the ability to include different custom configuration snippets in different places.
You can add your custom configuration snippet files at `/data/nginx/custom` as follow:
- `/data/nginx/custom/root_top.conf`: Included at the top of nginx.conf
- `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf
- `/data/nginx/custom/http_top.conf`: Included at the top of the main http block
- `/data/nginx/custom/http.conf`: Included at the end of the main http block
- `/data/nginx/custom/events.conf`: Included at the end of the events block
- `/data/nginx/custom/stream.conf`: Included at the end of the main stream block
- `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block
- `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block
- `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block
- `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block
- `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block
- `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block
Every file is optional.
## X-FRAME-OPTIONS Header
You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header
value by specifying it as a Docker environment variable. The default if not specified is `deny`.
```yml
...
environment:
X_FRAME_OPTIONS: "sameorigin"
...
```
## Customising logrotate settings
By default, NPM rotates the access- and error logs weekly and keeps 4 and 10 log files respectively.
Depending on the usage, this can lead to large log files, especially access logs.
You can customise the logrotate configuration through a mount (if your custom config is `logrotate.custom`):
```yml
volumes:
...
- ./logrotate.custom:/etc/logrotate.d/nginx-proxy-manager
```
For 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).
## Enabling the geoip2 module
To enable the geoip2 module, you can create the custom configuration file `/data/nginx/custom/root_top.conf` and include the following snippet:
```
load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so;
load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;
```
## Auto Initial User Creation
Setting these environment variables will create the default user on startup, skipping the UI first user setup screen:
```yml
environment:
INITIAL_ADMIN_EMAIL: my@example.com
INITIAL_ADMIN_PASSWORD: mypassword1
```
## Disable Nginx Resolver
On startup, we generate a resolvers directive for Nginx unless this is defined:
```yml
environment:
DISABLE_RESOLVER: true
```
In this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file
and then the `/etc/resolv.conf`.
================================================
FILE: docs/src/faq/index.md
================================================
---
outline: deep
---
# FAQ
## Do I have to use Docker?
Yes, that's how this project is packaged.
This makes it easier to support the project when we have control over the version of Nginx other packages
use by the project.
## Can I run it on a Raspberry Pi?
Yes! The docker image is multi-arch and is built for a variety of architectures. If yours is
[not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a
[GitHub issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).
## I can't get my service to proxy properly?
Your best bet is to ask the [Reddit community for support](https://www.reddit.com/r/nginxproxymanager/). There's safety in numbers.
## When adding username and password access control to a proxy host, I can no longer login into the app.
Having an Access Control List (ACL) with username and password requires the browser to always send this username
and password in the `Authorization` header on each request. If your proxied app also requires authentication (like
Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information,
as this is the standardized header meant for this kind of information. However having multiples of the same headers
is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps
do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can
only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization.
================================================
FILE: docs/src/guide/index.md
================================================
---
outline: deep
---
# Guide
::: raw
:::
This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup)
- [Full Setup](/setup/)
- [Screenshots](/screenshots/)
## Project Goal
I created this project to fill a personal need to provide users with an easy way to accomplish reverse
proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.
While there might be advanced options they are optional and the project should be as simple as possible
so that the barrier for entry here is low.
::: raw
:::
## Features
- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.io/)
- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx
- Free SSL using Let's Encrypt or provide your own custom SSL certificates
- Access Lists and basic HTTP Authentication for your hosts
- Advanced Nginx configuration available for super users
- User management, permissions and audit log
## Hosting your home network
I won't go in to too much detail here but here are the basics for someone new to this self-hosted world.
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project
3. 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)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/get-docker/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
environment:
TZ: "Australia/Brisbane"
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.
3. Bring up your stack by running
```bash
docker compose up -d
```
4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface.
[http://127.0.0.1:81](http://127.0.0.1:81)
This startup can take a minute depending on your hardware.
## Contributing
All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch.
CI is used in this project. All PR's must pass before being considered. After passing,
docker builds for PR's are available on dockerhub for manual verifications.
Documentation within the `develop` branch is available for preview at
[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com)
### Contributors
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).
## Getting Support
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)
3. [Reddit](https://reddit.com/r/nginxproxymanager)
================================================
FILE: docs/src/index.md
================================================
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Nginx Proxy Manager"
tagline: Expose your services easily and securely
image:
src: /logo.svg
alt: NPM Logo
actions:
- theme: brand
text: Get Started
link: /guide/
- theme: alt
text: GitHub
link: https://github.com/NginxProxyManager/nginx-proxy-manager
features:
- title: Get Connected
details: Expose web services on your network · Free SSL with Let's Encrypt · Designed with security in mind · Perfect for home networks
- title: Proxy Hosts
details: Expose your private network Web services and get connected anywhere.
- title: Beautiful UI
details: Based on Tabler, the interface is a pleasure to use. Configuring a server has never been so fun.
- title: Free SSL
details: Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves!
- title: Docker FTW
details: Built as a Docker Image, Nginx Proxy Manager only requires a database.
- title: Multiple Users
details: Configure other users to either view or manage their own hosts. Full access permissions are available.
---
================================================
FILE: docs/src/public/robots.txt
================================================
User-agent: *
Disallow:
================================================
FILE: docs/src/screenshots/index.md
================================================
---
outline: deep
---
# Screenshots
### Light Mode
::: raw
:::
### Dark Mode
::: raw
:::
================================================
FILE: docs/src/setup/index.md
================================================
---
outline: deep
---
# Full Setup Instructions
## Running the App
Create a `docker-compose.yml` file:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
ports:
# These ports are in format :
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
environment:
TZ: "Australia/Brisbane"
# Uncomment this if you want to change the location of
# the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
Then:
```bash
docker compose up -d
```
## Using MySQL / MariaDB Database
If you opt for the MySQL configuration you will have to provide the database server yourself.
It'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
are going to use.
Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
ports:
# These ports are in format :
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
environment:
TZ: "Australia/Brisbane"
# Mysql/Maria connection parameters:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
# Optional SSL (see section below)
# DB_MYSQL_SSL: 'true'
# DB_MYSQL_SSL_REJECT_UNAUTHORIZED: 'true'
# DB_MYSQL_SSL_VERIFY_IDENTITY: 'true'
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: 'linuxserver/mariadb'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
TZ: 'Australia/Brisbane'
volumes:
- ./mariadb:/config
```
::: warning
Please 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.
:::
### Optional: MySQL / MariaDB SSL
You can enable TLS for the MySQL/MariaDB connection with these environment variables:
- `DB_MYSQL_SSL`: Enable SSL when set to true. If unset or false, SSL disabled (previous default behaviour).
- `DB_MYSQL_SSL_REJECT_UNAUTHORIZED`: (default: true) Validate the server certificate chain. Set to false to allow self‑signed/unknown CA.
- `DB_MYSQL_SSL_VERIFY_IDENTITY`: (default: true) Performs host name / identity verification.
Enabling SSL using a self-signed cert (not recommended for production).
## Using Postgres database
Similar to the MySQL server setup:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
ports:
# These ports are in format :
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
environment:
TZ: "Australia/Brisbane"
# Postgres parameters:
DB_POSTGRES_HOST: 'db'
DB_POSTGRES_PORT: '5432'
DB_POSTGRES_USER: 'npm'
DB_POSTGRES_PASSWORD: 'npmpass'
DB_POSTGRES_NAME: 'npm'
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_USER: 'npm'
POSTGRES_PASSWORD: 'npmpass'
POSTGRES_DB: 'npm'
volumes:
- ./postgresql:/var/lib/postgresql
```
::: warning
Custom Postgres schema is not supported, as such `public` will be used.
:::
## Running on Raspberry PI / ARM devices
The docker images support the following architectures:
- amd64
- arm64
::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
use the `2.13.7` image tag if this applies to you.
:::
The docker images are a manifest of all the architecture docker builds supported, so this means
you don't have to worry about doing anything special and you can follow the common instructions above.
Check out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags)
for a list of supported architectures and if you want one that doesn't exist,
[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=).
Also, 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/)
on Raspbian.
## Initial Run
After the app is running for the first time, the following will happen:
1. JWT keys will be generated and saved in the data folder
2. The database will initialize with table structures
3. A default admin user will be created
This process can take a couple of minutes depending on your machine.
================================================
FILE: docs/src/third-party/index.md
================================================
---
outline: deep
---
# Third Party
As 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
integrations, they are *not supported* by me.
Known integrations:
- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager)
- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager)
- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager)
- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager)
- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf)
If you would like your integration of NPM listed, please open a
[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)
================================================
FILE: docs/src/upgrading/index.md
================================================
---
outline: deep
---
# Upgrading
```bash
docker compose pull
docker compose up -d
```
This project will automatically update any databases or other requirements so you don't have to follow
any crazy instructions. These steps above will pull the latest updates and recreate the docker
containers.
See the [list of releases](https://github.com/NginxProxyManager/nginx-proxy-manager/releases) for any upgrade steps specific to each release.
================================================
FILE: frontend/.gitignore
================================================
src/locale/lang
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: frontend/biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"!**/dist/**/*"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"formatWithErrors": true
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":BUN:",
":NODE:",
[
"npm:*",
"npm:*/**"
],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
[
"/src/*",
"/src/**"
],
[
"/**"
],
[
"#*",
"#*/**"
],
":PATH:"
]
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useUniqueElementIds": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noArrayIndexKey": "off"
},
"performance": {
"noDelete": "off"
},
"nursery": "off",
"a11y": {
"useSemanticElements": "off",
"useValidAnchor": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
}
================================================
FILE: frontend/check-locales.cjs
================================================
#!/usr/bin/env node
// This file does a few things to ensure that the Locales are present and valid:
// - Ensures that the name of the locale exists in the language list
// - Ensures that each locale contains the translations used in the application
// - Ensures that there are no unused translations in the locale files
// - Also checks the error messages returned by the backend
const allLocales = [
["en", "en-US"],
["de", "de-DE"],
["pt", "pt-PT"],
["es", "es-ES"],
["et", "et-EE"],
["fr", "fr-FR"],
["it", "it-IT"],
["ja", "ja-JP"],
["nl", "nl-NL"],
["pl", "pl-PL"],
["ru", "ru-RU"],
["sk", "sk-SK"],
["cs", "cs-CZ"],
["vi", "vi-VN"],
["zh", "zh-CN"],
["ko", "ko-KR"],
["bg", "bg-BG"],
["id", "id-ID"],
["tr", "tr-TR"],
["hu", "hu-HU"],
["no", "no-NO"],
];
const ignoreUnused = [/^.*$/];
const { spawnSync } = require("child_process");
const fs = require("fs");
const tmp = require("tmp");
// Parse backend errors
const BACKEND_ERRORS_FILE = "../backend/internal/errors/errors.go";
const BACKEND_ERRORS = [];
/*
try {
const backendErrorsContent = fs.readFileSync(BACKEND_ERRORS_FILE, "utf8");
const backendErrorsContentRes = [
...backendErrorsContent.matchAll(/(?:errors|eris)\.New\("([^"]+)"\)/g),
];
backendErrorsContentRes.map((item) => {
BACKEND_ERRORS.push("error." + item[1]);
return null;
});
} catch (err) {
console.log("\x1b[31m%s\x1b[0m", err);
process.exit(1);
}
*/
// get all translations used in frontend code
const tmpobj = tmp.fileSync({ postfix: ".json" });
spawnSync("yarn", ["locale-extract", "--out-file", tmpobj.name]);
const allLocalesInProject = require(tmpobj.name);
// get list og language names and locales
const langList = require("./src/locale/src/lang-list.json");
// store a list of all validation errors
const allErrors = [];
const allWarnings = [];
const allKeys = [];
const checkLangList = (fullCode) => {
const key = "locale-" + fullCode;
if (typeof langList[key] === "undefined") {
allErrors.push("ERROR: `" + key + "` language does not exist in lang-list.json");
}
};
const compareLocale = (locale) => {
const projectLocaleKeys = Object.keys(allLocalesInProject);
// Check that locale contains the items used in the codebase
projectLocaleKeys.map((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
// Check that locale contains all error.* items
BACKEND_ERRORS.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
// Check that locale does not contain items not used in the codebase
const localeKeys = Object.keys(locale.data);
localeKeys.map((key) => {
let ignored = false;
ignoreUnused.map((regex) => {
if (key.match(regex)) {
ignored = true;
}
return null;
});
if (!ignored && typeof allLocalesInProject[key] === "undefined") {
// ensure this key doesn't exist in the backend errors either
if (!BACKEND_ERRORS.includes(key)) {
allErrors.push("ERROR: `" + locale[0] + "` contains unused item: `" + key + "`");
}
}
// Add this key to allKeys
if (allKeys.indexOf(key) === -1) {
allKeys.push(key);
}
return null;
});
};
// Checks for any keys missing from this locale, that
// have been defined in any other locales
const checkForMissing = (locale) => {
allKeys.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allWarnings.push("WARN: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
};
// Local all locale data
allLocales.map((locale, idx) => {
checkLangList(locale[1]);
allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json");
return null;
});
// Verify all locale data
allLocales.map((locale) => {
compareLocale(locale);
checkForMissing(locale);
return null;
});
if (allErrors.length) {
allErrors.map((err) => {
console.log("\x1b[31m%s\x1b[0m", err);
return null;
});
}
if (allWarnings.length) {
allWarnings.map((err) => {
console.log("\x1b[33m%s\x1b[0m", err);
return null;
});
}
if (allErrors.length) {
process.exit(1);
}
console.log("\x1b[32m%s\x1b[0m", "Locale check passed");
process.exit(0);
================================================
FILE: frontend/index.html
================================================
Nginx Proxy Manager
You need to enable JavaScript to run this app.
================================================
FILE: frontend/package.json
================================================
{
"name": "nginx-proxy-manager",
"version": "2.0.0",
"type": "module",
"author": "Jamie Curnow ",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "biome lint",
"preview": "vite preview",
"prettier": "biome format --write ./src",
"locale-extract": "formatjs extract 'src/**/*.tsx'",
"locale-compile": "formatjs compile-folder src/locale/src src/locale/lang",
"locale-sort": "./src/locale/scripts/locale-sort.sh",
"test": "vitest"
},
"dependencies": {
"@tabler/core": "^1.4.0",
"@tabler/icons-react": "^3.38.0",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-textarea-code-editor": "^3.1.1",
"classnames": "^2.5.1",
"country-flag-icons": "^1.6.15",
"date-fns": "^4.1.0",
"ez-modal-react": "^1.0.5",
"formik": "^2.4.9",
"generate-password-browser": "^1.1.0",
"humps": "^2.0.1",
"query-string": "^9.3.1",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.4",
"react-intl": "^8.1.3",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.1",
"react-select": "^5.10.2",
"react-toastify": "^11.0.5",
"rooks": "^9.5.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.5",
"@formatjs/cli": "^6.13.0",
"@tanstack/react-query-devtools": "^5.91.3",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/country-flag-icons": "^1.2.2",
"@types/humps": "^2.0.6",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^5.1.4",
"happy-dom": "^20.8.3",
"postcss": "^8.5.8",
"postcss-simple-vars": "^7.0.1",
"sass": "^1.97.3",
"tmp": "^0.2.5",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vite-plugin-checker": "^0.12.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18"
}
}
================================================
FILE: frontend/public/images/favicon/browserconfig.xml
================================================
#333333
================================================
FILE: frontend/public/images/favicon/site.webmanifest
================================================
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/images/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: frontend/src/App.css
================================================
:root {
color-scheme: light dark;
}
.light {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
.modal-backdrop {
--tblr-backdrop-opacity: 0.8 !important;
}
[data-bs-theme="dark"] .modal-content {
--tblr-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
[data-bs-theme="dark"] .modal-backdrop {
--tblr-backdrop-bg: #000 !important;
--tblr-backdrop-opacity: 0.65 !important;
}
.domain-name {
font-family: monospace;
}
.mr-1 {
margin-right: 0.25rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.react-select-container {
.react-select__control {
color: var(--tblr-body-color);
background-color: var(--tblr-bg-forms);
border: var(--tblr-border-width) solid var(--tblr-border-color);
.react-select__input {
color: var(--tblr-body-color) !important;
}
.react-select__single-value {
color: var(--tblr-body-color);
}
.react-select__multi-value {
border: 1px solid var(--tblr-border-color);
background-color: var(--tblr-bg-surface-tertiary);
color: var(--tblr-secondary) !important;
.react-select__multi-value__label {
color: var(--tblr-secondary) !important;
}
}
}
.react-select__menu {
background-color: var(--tblr-bg-forms);
.react-select__option {
background: rgba(var(--tblr-primary-rgb), .04);
color: inherit !important;
&.react-select__option--is-focused {
background: rgba(var(--tblr-primary-rgb), .1);
}
&.react-select__option--is-focused.react-select__option--is-selected {
background: rgba(var(--tblr-primary-rgb), .2);
}
}
}
}
.textareaMono {
font-family: 'Courier New', Courier, monospace !important;
resize: vertical;
}
label.row {
cursor: pointer;
}
.input-group-select {
display: flex;
align-items: center;
padding: 0;
font-size: .875rem;
font-weight: 400;
line-height: 1.25rem;
color: var(--tblr-gray-500);
text-align: center;
white-space: nowrap;
background-color: var(--tblr-bg-surface-secondary);
border: var(--tblr-border-width) solid var(--tblr-border-color);
border-radius: var(--tblr-border-radius);
.form-select {
border: none;
background-color: var(--tblr-bg-surface-secondary);
border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius);
}
}
/* Fix for dropdown menus being clipped by table-responsive containers. */
.table-responsive .dropdown {
position: static;
}
/* Fix for Tabler scrollbar compensation */
@media (min-width: 992px) {
:host, :root {
margin-left: 0;
}
}
================================================
FILE: frontend/src/App.tsx
================================================
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import EasyModal from "ez-modal-react";
import { RawIntlProvider } from "react-intl";
import { ToastContainer } from "react-toastify";
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
import { intl } from "src/locale";
import Router from "src/Router.tsx";
// Create a client
const queryClient = new QueryClient();
function App() {
return (
);
}
export default App;
================================================
FILE: frontend/src/Router.tsx
================================================
import { lazy, Suspense } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import {
ErrorNotFound,
LoadingPage,
Page,
SiteContainer,
SiteFooter,
SiteHeader,
SiteMenu,
Unhealthy,
} from "src/components";
import { useAuthState } from "src/context";
import { useHealth } from "src/hooks";
const Setup = lazy(() => import("src/pages/Setup"));
const Login = lazy(() => import("src/pages/Login"));
const Dashboard = lazy(() => import("src/pages/Dashboard"));
const Settings = lazy(() => import("src/pages/Settings"));
const Certificates = lazy(() => import("src/pages/Certificates"));
const Access = lazy(() => import("src/pages/Access"));
const AuditLog = lazy(() => import("src/pages/AuditLog"));
const Users = lazy(() => import("src/pages/Users"));
const ProxyHosts = lazy(() => import("src/pages/Nginx/ProxyHosts"));
const RedirectionHosts = lazy(() => import("src/pages/Nginx/RedirectionHosts"));
const DeadHosts = lazy(() => import("src/pages/Nginx/DeadHosts"));
const Streams = lazy(() => import("src/pages/Nginx/Streams"));
function Router() {
const health = useHealth();
const { authenticated } = useAuthState();
if (health.isLoading) {
return ;
}
if (health.isError || health.data?.status !== "OK") {
return ;
}
if (!health.data?.setup) {
return ;
}
if (!authenticated) {
return (
}>
);
}
return (
}>
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
);
}
export default Router;
================================================
FILE: frontend/src/api/backend/base.ts
================================================
import { QueryClient } from "@tanstack/react-query";
import { camelizeKeys, decamelize, decamelizeKeys } from "humps";
import queryString, { type StringifiableRecord } from "query-string";
import AuthStore from "src/modules/AuthStore";
const queryClient = new QueryClient();
const contentTypeHeader = "Content-Type";
interface BuildUrlArgs {
url: string;
params?: StringifiableRecord;
}
function decamelizeParams(params?: StringifiableRecord): StringifiableRecord | undefined {
if (!params) {
return undefined;
}
const result: StringifiableRecord = {};
for (const [key, value] of Object.entries(params)) {
result[decamelize(key)] = value;
}
return result;
}
function buildUrl({ url, params }: BuildUrlArgs) {
const endpoint = url.replace(/^\/|\/$/g, "");
const baseUrl = `/api/${endpoint}`;
const apiUrl = queryString.stringifyUrl({
url: baseUrl,
query: decamelizeParams(params),
});
return apiUrl;
}
function buildAuthHeader(): Record | undefined {
if (AuthStore.token) {
return { Authorization: `Bearer ${AuthStore.token.token}` };
}
return {};
}
function buildBody(data?: Record): string | undefined {
if (data) {
return JSON.stringify(decamelizeKeys(data));
}
}
async function processResponse(response: Response) {
const payload = await response.json();
if (!response.ok) {
if (response.status === 401) {
// Force logout user and reload the page if Unauthorized
AuthStore.clear();
queryClient.clear();
window.location.reload();
}
throw new Error(
typeof payload.error.messageI18n !== "undefined" ? payload.error.messageI18n : payload.error.message,
);
}
return camelizeKeys(payload) as any;
}
interface GetArgs {
url: string;
params?: queryString.StringifiableRecord;
}
async function baseGet({ url, params }: GetArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params });
const method = "GET";
const headers = buildAuthHeader();
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, signal });
return response;
}
export async function get(args: GetArgs, abortController?: AbortController) {
return processResponse(await baseGet(args, abortController));
}
export async function download({ url, params }: GetArgs, filename = "download.file") {
const headers = buildAuthHeader();
const res = await fetch(buildUrl({ url, params }), { headers });
const bl = await res.blob();
const u = window.URL.createObjectURL(bl);
const a = document.createElement("a");
a.href = u;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
}
interface PostArgs {
url: string;
params?: queryString.StringifiableRecord;
data?: any;
noAuth?: boolean;
}
export async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params });
const method = "POST";
let headers: Record = {};
if (!noAuth) {
headers = {
...buildAuthHeader(),
};
}
let body: string | FormData | undefined;
// Check if the data is an instance of FormData
// If data is FormData, let the browser set the Content-Type header
if (data instanceof FormData) {
body = data;
} else {
// If data is JSON, set the Content-Type header to 'application/json'
headers = {
...headers,
[contentTypeHeader]: "application/json",
};
body = buildBody(data);
}
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, body, signal });
return processResponse(response);
}
interface PutArgs {
url: string;
params?: queryString.StringifiableRecord;
data?: Record;
}
export async function put({ url, params, data }: PutArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params });
const method = "PUT";
const headers = {
...buildAuthHeader(),
[contentTypeHeader]: "application/json",
};
const signal = abortController?.signal;
const body = buildBody(data);
const response = await fetch(apiUrl, { method, headers, body, signal });
return processResponse(response);
}
interface DeleteArgs {
url: string;
params?: queryString.StringifiableRecord;
}
export async function del({ url, params }: DeleteArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params });
const method = "DELETE";
const headers = {
...buildAuthHeader(),
};
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, signal });
return processResponse(response);
}
================================================
FILE: frontend/src/api/backend/checkVersion.ts
================================================
import * as api from "./base";
import type { VersionCheckResponse } from "./responseTypes";
export async function checkVersion(): Promise {
return await api.get({
url: "/version/check",
});
}
================================================
FILE: frontend/src/api/backend/createAccessList.ts
================================================
import * as api from "./base";
import type { AccessList } from "./models";
export async function createAccessList(item: AccessList): Promise {
return await api.post({
url: "/nginx/access-lists",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createCertificate.ts
================================================
import * as api from "./base";
import type { Certificate } from "./models";
export async function createCertificate(item: Certificate): Promise {
return await api.post({
url: "/nginx/certificates",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createDeadHost.ts
================================================
import * as api from "./base";
import type { DeadHost } from "./models";
export async function createDeadHost(item: DeadHost): Promise {
return await api.post({
url: "/nginx/dead-hosts",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createProxyHost.ts
================================================
import * as api from "./base";
import type { ProxyHost } from "./models";
export async function createProxyHost(item: ProxyHost): Promise {
return await api.post({
url: "/nginx/proxy-hosts",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createRedirectionHost.ts
================================================
import * as api from "./base";
import type { RedirectionHost } from "./models";
export async function createRedirectionHost(item: RedirectionHost): Promise {
return await api.post({
url: "/nginx/redirection-hosts",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createStream.ts
================================================
import * as api from "./base";
import type { Stream } from "./models";
export async function createStream(item: Stream): Promise {
return await api.post({
url: "/nginx/streams",
data: item,
});
}
================================================
FILE: frontend/src/api/backend/createUser.ts
================================================
import * as api from "./base";
import type { User } from "./models";
export interface AuthOptions {
type: string;
secret: string;
}
export interface NewUser {
name: string;
nickname: string;
email: string;
isDisabled?: boolean;
auth?: AuthOptions;
roles?: string[];
}
export async function createUser(item: NewUser, noAuth?: boolean): Promise {
return await api.post({
url: "/users",
data: item,
noAuth,
});
}
================================================
FILE: frontend/src/api/backend/deleteAccessList.ts
================================================
import * as api from "./base";
export async function deleteAccessList(id: number): Promise {
return await api.del({
url: `/nginx/access-lists/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteCertificate.ts
================================================
import * as api from "./base";
export async function deleteCertificate(id: number): Promise {
return await api.del({
url: `/nginx/certificates/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteDeadHost.ts
================================================
import * as api from "./base";
export async function deleteDeadHost(id: number): Promise {
return await api.del({
url: `/nginx/dead-hosts/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteProxyHost.ts
================================================
import * as api from "./base";
export async function deleteProxyHost(id: number): Promise {
return await api.del({
url: `/nginx/proxy-hosts/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteRedirectionHost.ts
================================================
import * as api from "./base";
export async function deleteRedirectionHost(id: number): Promise {
return await api.del({
url: `/nginx/redirection-hosts/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteStream.ts
================================================
import * as api from "./base";
export async function deleteStream(id: number): Promise {
return await api.del({
url: `/nginx/streams/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/deleteUser.ts
================================================
import * as api from "./base";
export async function deleteUser(id: number): Promise {
return await api.del({
url: `/users/${id}`,
});
}
================================================
FILE: frontend/src/api/backend/downloadCertificate.ts
================================================
import * as api from "./base";
export async function downloadCertificate(id: number): Promise {
await api.download(
{
url: `/nginx/certificates/${id}/download`,
},
`certificate-${id}.zip`,
);
}
================================================
FILE: frontend/src/api/backend/expansions.ts
================================================
export type AccessListExpansion = "owner" | "items" | "clients";
export type AuditLogExpansion = "user";
export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts" | "streams";
export type HostExpansion = "owner" | "certificate";
export type ProxyHostExpansion = "owner" | "access_list" | "certificate";
export type UserExpansion = "permissions";
================================================
FILE: frontend/src/api/backend/getAccessList.ts
================================================
import * as api from "./base";
import type { AccessListExpansion } from "./expansions";
import type { AccessList } from "./models";
export async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/access-lists/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getAccessLists.ts
================================================
import * as api from "./base";
import type { AccessListExpansion } from "./expansions";
import type { AccessList } from "./models";
export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/access-lists",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getAuditLog.ts
================================================
import * as api from "./base";
import type { AuditLogExpansion } from "./expansions";
import type { AuditLog } from "./models";
export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise {
return await api.get({
url: `/audit-log/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getAuditLogs.ts
================================================
import * as api from "./base";
import type { AuditLogExpansion } from "./expansions";
import type { AuditLog } from "./models";
export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise {
return await api.get({
url: "/audit-log",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getCertificate.ts
================================================
import * as api from "./base";
import type { CertificateExpansion } from "./expansions";
import type { Certificate } from "./models";
export async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/certificates/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getCertificateDNSProviders.ts
================================================
import * as api from "./base";
import type { DNSProvider } from "./models";
export async function getCertificateDNSProviders(params = {}): Promise {
return await api.get({
url: "/nginx/certificates/dns-providers",
params,
});
}
================================================
FILE: frontend/src/api/backend/getCertificates.ts
================================================
import * as api from "./base";
import type { CertificateExpansion } from "./expansions";
import type { Certificate } from "./models";
export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/certificates",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getDeadHost.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { DeadHost } from "./models";
export async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/dead-hosts/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getDeadHosts.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { DeadHost } from "./models";
export async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/dead-hosts",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getHealth.ts
================================================
import * as api from "./base";
import type { HealthResponse } from "./responseTypes";
export async function getHealth(): Promise {
return await api.get({
url: "/",
});
}
================================================
FILE: frontend/src/api/backend/getHostsReport.ts
================================================
import * as api from "./base";
export async function getHostsReport(): Promise> {
return await api.get({
url: "/reports/hosts",
});
}
================================================
FILE: frontend/src/api/backend/getProxyHost.ts
================================================
import * as api from "./base";
import type { ProxyHostExpansion } from "./expansions";
import type { ProxyHost } from "./models";
export async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/proxy-hosts/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getProxyHosts.ts
================================================
import * as api from "./base";
import type { ProxyHostExpansion } from "./expansions";
import type { ProxyHost } from "./models";
export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/proxy-hosts",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getRedirectionHost.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { RedirectionHost } from "./models";
export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/redirection-hosts/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getRedirectionHosts.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { RedirectionHost } from "./models";
export async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/redirection-hosts",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getSetting.ts
================================================
import * as api from "./base";
import type { Setting } from "./models";
export async function getSetting(id: string, expand?: string[], params = {}): Promise {
return await api.get({
url: `/settings/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getSettings.ts
================================================
import * as api from "./base";
import type { Setting } from "./models";
export async function getSettings(expand?: string[], params = {}): Promise {
return await api.get({
url: "/settings",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getStream.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { Stream } from "./models";
export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: `/nginx/streams/${id}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getStreams.ts
================================================
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { Stream } from "./models";
export async function getStreams(expand?: HostExpansion[], params = {}): Promise {
return await api.get({
url: "/nginx/streams",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getToken.ts
================================================
import * as api from "./base";
import type { TokenResponse, TwoFactorChallengeResponse } from "./responseTypes";
export type LoginResponse = TokenResponse | TwoFactorChallengeResponse;
export function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse {
return "requires2fa" in response && response.requires2fa === true;
}
export async function getToken(identity: string, secret: string): Promise {
return await api.post({
url: "/tokens",
data: { identity, secret },
});
}
export async function verify2FA(challengeToken: string, code: string): Promise {
return await api.post({
url: "/tokens/2fa",
data: { challengeToken, code },
});
}
================================================
FILE: frontend/src/api/backend/getUser.ts
================================================
import * as api from "./base";
import type { UserExpansion } from "./expansions";
import type { User } from "./models";
export async function getUser(id: number | string = "me", expand?: UserExpansion[], params = {}): Promise {
const userId = id ? id : "me";
return await api.get({
url: `/users/${userId}`,
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/getUsers.ts
================================================
import * as api from "./base";
import type { UserExpansion } from "./expansions";
import type { User } from "./models";
export async function getUsers(expand?: UserExpansion[], params = {}): Promise {
return await api.get({
url: "/users",
params: {
expand: expand?.join(","),
...params,
},
});
}
================================================
FILE: frontend/src/api/backend/helpers.ts
================================================
import { decamelize } from "humps";
/**
* This will convert a react-table sort object into
* a string that the backend api likes:
* name.asc,id.desc
*/
export function tableSortToAPI(sortBy: any): string | undefined {
if (sortBy?.length > 0) {
const strs: string[] = [];
sortBy.map((item: any) => {
strs.push(`${decamelize(item.id)}.${item.desc ? "desc" : "asc"}`);
return undefined;
});
return strs.join(",");
}
return;
}
/**
* This will convert a react-table filters object into
* a string that the backend api likes:
* name:contains=jam
*/
export function tableFiltersToAPI(filters: any[]): { [key: string]: string } {
const items: { [key: string]: string } = {};
if (filters?.length > 0) {
filters.map((item: any) => {
items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value;
return undefined;
});
}
return items;
}
/**
* Builds a filters object by removing entries with undefined, null, or empty string values.
*
*/
export function buildFilters(filters?: Record) {
if (!filters) {
return filters;
}
const result: Record = {};
for (const key in filters) {
const value = filters[key];
// If the value is undefined, null, or an empty string, skip it
if (value === undefined || value === null || value === "") {
continue;
}
result[key] = value.toString();
}
return result;
}
================================================
FILE: frontend/src/api/backend/index.ts
================================================
export * from "./checkVersion";
export * from "./createAccessList";
export * from "./createCertificate";
export * from "./createDeadHost";
export * from "./createProxyHost";
export * from "./createRedirectionHost";
export * from "./createStream";
export * from "./createUser";
export * from "./deleteAccessList";
export * from "./deleteCertificate";
export * from "./deleteDeadHost";
export * from "./deleteProxyHost";
export * from "./deleteRedirectionHost";
export * from "./deleteStream";
export * from "./deleteUser";
export * from "./downloadCertificate";
export * from "./expansions";
export * from "./getAccessList";
export * from "./getAccessLists";
export * from "./getAuditLog";
export * from "./getAuditLogs";
export * from "./getCertificate";
export * from "./getCertificateDNSProviders";
export * from "./getCertificates";
export * from "./getDeadHost";
export * from "./getDeadHosts";
export * from "./getHealth";
export * from "./getHostsReport";
export * from "./getProxyHost";
export * from "./getProxyHosts";
export * from "./getRedirectionHost";
export * from "./getRedirectionHosts";
export * from "./getSetting";
export * from "./getSettings";
export * from "./getStream";
export * from "./getStreams";
export * from "./getToken";
export * from "./getUser";
export * from "./getUsers";
export * from "./helpers";
export * from "./loginAsUser";
export * from "./models";
export * from "./refreshToken";
export * from "./renewCertificate";
export * from "./responseTypes";
export * from "./setPermissions";
export * from "./testHttpCertificate";
export * from "./toggleDeadHost";
export * from "./toggleProxyHost";
export * from "./toggleRedirectionHost";
export * from "./toggleStream";
export * from "./toggleUser";
export * from "./updateAccessList";
export * from "./updateAuth";
export * from "./updateDeadHost";
export * from "./updateProxyHost";
export * from "./updateRedirectionHost";
export * from "./updateSetting";
export * from "./updateStream";
export * from "./updateUser";
export * from "./uploadCertificate";
export * from "./validateCertificate";
export * from "./twoFactor";
================================================
FILE: frontend/src/api/backend/loginAsUser.ts
================================================
import * as api from "./base";
import type { LoginAsTokenResponse } from "./responseTypes";
export async function loginAsUser(id: number): Promise {
return await api.post({
url: `/users/${id}/login`,
});
}
================================================
FILE: frontend/src/api/backend/models.ts
================================================
export interface AppVersion {
major: number;
minor: number;
revision: number;
}
export interface UserPermissions {
id?: number;
createdOn?: string;
modifiedOn?: string;
userId?: number;
visibility: string;
proxyHosts: string;
redirectionHosts: string;
deadHosts: string;
streams: string;
accessLists: string;
certificates: string;
}
export interface User {
id: number;
createdOn: string;
modifiedOn: string;
isDisabled: boolean;
email: string;
name: string;
nickname: string;
avatar: string;
roles: string[];
permissions?: UserPermissions;
}
export interface AuditLog {
id: number;
createdOn: string;
modifiedOn: string;
userId: number;
objectType: string;
objectId: number;
action: string;
meta: Record;
// Expansions:
user?: User;
}
export interface AccessList {
id?: number;
createdOn?: string;
modifiedOn?: string;
ownerUserId: number;
name: string;
meta: Record;
satisfyAny: boolean;
passAuth: boolean;
proxyHostCount?: number;
// Expansions:
owner?: User;
items?: AccessListItem[];
clients?: AccessListClient[];
}
export interface AccessListItem {
id?: number;
createdOn?: string;
modifiedOn?: string;
accessListId?: number;
username: string;
password: string;
meta?: Record;
hint?: string;
}
export type AccessListClient = {
id?: number;
createdOn?: string;
modifiedOn?: string;
accessListId?: number;
address: string;
directive: "allow" | "deny";
meta?: Record;
};
export interface Certificate {
id: number;
createdOn: string;
modifiedOn: string;
ownerUserId: number;
provider: string;
niceName: string;
domainNames: string[];
expiresOn: string;
meta: Record;
owner?: User;
proxyHosts?: ProxyHost[];
deadHosts?: DeadHost[];
redirectionHosts?: RedirectionHost[];
}
export interface ProxyLocation {
path: string;
advancedConfig: string;
forwardScheme: string;
forwardHost: string;
forwardPort: number;
}
export interface ProxyHost {
id: number;
createdOn: string;
modifiedOn: string;
ownerUserId: number;
domainNames: string[];
forwardScheme: string;
forwardHost: string;
forwardPort: number;
accessListId: number;
certificateId: number;
sslForced: boolean;
cachingEnabled: boolean;
blockExploits: boolean;
advancedConfig: string;
meta: Record;
allowWebsocketUpgrade: boolean;
http2Support: boolean;
enabled: boolean;
locations?: ProxyLocation[];
hstsEnabled: boolean;
hstsSubdomains: boolean;
trustForwardedProto: boolean;
// Expansions:
owner?: User;
accessList?: AccessList;
certificate?: Certificate;
}
export interface DeadHost {
id: number;
createdOn: string;
modifiedOn: string;
ownerUserId: number;
domainNames: string[];
certificateId: number;
sslForced: boolean;
advancedConfig: string;
meta: Record;
http2Support: boolean;
enabled: boolean;
hstsEnabled: boolean;
hstsSubdomains: boolean;
// Expansions:
owner?: User;
certificate?: Certificate;
}
export interface RedirectionHost {
id: number;
createdOn: string;
modifiedOn: string;
ownerUserId: number;
domainNames: string[];
forwardDomainName: string;
preservePath: boolean;
certificateId: number;
sslForced: boolean;
blockExploits: boolean;
advancedConfig: string;
meta: Record;
http2Support: boolean;
forwardScheme: string;
forwardHttpCode: number;
enabled: boolean;
hstsEnabled: boolean;
hstsSubdomains: boolean;
// Expansions:
owner?: User;
certificate?: Certificate;
}
export interface Stream {
id: number;
createdOn: string;
modifiedOn: string;
ownerUserId: number;
incomingPort: number;
forwardingHost: string;
forwardingPort: number;
tcpForwarding: boolean;
udpForwarding: boolean;
meta: Record;
enabled: boolean;
certificateId: number;
// Expansions:
owner?: User;
certificate?: Certificate;
}
export interface Setting {
id: string;
name?: string;
description?: string;
value: string;
meta?: Record;
}
export interface DNSProvider {
id: string;
name: string;
credentials: string;
}
================================================
FILE: frontend/src/api/backend/refreshToken.ts
================================================
import * as api from "./base";
import type { TokenResponse } from "./responseTypes";
export async function refreshToken(): Promise {
return await api.get({
url: "/tokens",
});
}
================================================
FILE: frontend/src/api/backend/renewCertificate.ts
================================================
import * as api from "./base";
import type { Certificate } from "./models";
export async function renewCertificate(id: number): Promise {
return await api.post({
url: `/nginx/certificates/${id}/renew`,
});
}
================================================
FILE: frontend/src/api/backend/responseTypes.ts
================================================
import type { AppVersion, User } from "./models";
export interface HealthResponse {
status: string;
version: AppVersion;
setup: boolean;
}
export interface TokenResponse {
expires: number;
token: string;
}
export interface ValidatedCertificateResponse {
certificate: Record;
certificateKey: boolean;
}
export interface LoginAsTokenResponse extends TokenResponse {
user: User;
}
export interface VersionCheckResponse {
current: string | null;
latest: string | null;
updateAvailable: boolean;
}
export interface TwoFactorChallengeResponse {
requires2fa: boolean;
challengeToken: string;
}
export interface TwoFactorStatusResponse {
enabled: boolean;
backupCodesRemaining: number;
}
export interface TwoFactorSetupResponse {
secret: string;
otpauthUrl: string;
}
export interface TwoFactorEnableResponse {
backupCodes: string[];
}
================================================
FILE: frontend/src/api/backend/setPermissions.ts
================================================
import * as api from "./base";
import type { UserPermissions } from "./models";
export async function setPermissions(userId: number, data: UserPermissions): Promise {
// Remove readonly fields
return await api.put({
url: `/users/${userId}/permissions`,
data,
});
}
================================================
FILE: frontend/src/api/backend/testHttpCertificate.ts
================================================
import * as api from "./base";
export async function testHttpCertificate(domains: string[]): Promise> {
return await api.post({
url: "/nginx/certificates/test-http",
data: {
domains,
},
});
}
================================================
FILE: frontend/src/api/backend/toggleDeadHost.ts
================================================
import * as api from "./base";
export async function toggleDeadHost(id: number, enabled: boolean): Promise {
return await api.post({
url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`,
});
}
================================================
FILE: frontend/src/api/backend/toggleProxyHost.ts
================================================
import * as api from "./base";
export async function toggleProxyHost(id: number, enabled: boolean): Promise {
return await api.post({
url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`,
});
}
================================================
FILE: frontend/src/api/backend/toggleRedirectionHost.ts
================================================
import * as api from "./base";
export async function toggleRedirectionHost(id: number, enabled: boolean): Promise {
return await api.post({
url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`,
});
}
================================================
FILE: frontend/src/api/backend/toggleStream.ts
================================================
import * as api from "./base";
export async function toggleStream(id: number, enabled: boolean): Promise {
return await api.post({
url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`,
});
}
================================================
FILE: frontend/src/api/backend/toggleUser.ts
================================================
import type { User } from "./models";
import { updateUser } from "./updateUser";
export async function toggleUser(id: number, enabled: boolean): Promise {
await updateUser({
id,
isDisabled: !enabled,
} as User);
return true;
}
================================================
FILE: frontend/src/api/backend/twoFactor.ts
================================================
import * as api from "./base";
import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes";
export async function get2FAStatus(userId: number | "me"): Promise {
return await api.get({
url: `/users/${userId}/2fa`,
});
}
export async function start2FASetup(userId: number | "me"): Promise {
return await api.post({
url: `/users/${userId}/2fa`,
});
}
export async function enable2FA(userId: number | "me", code: string): Promise {
return await api.post({
url: `/users/${userId}/2fa/enable`,
data: { code },
});
}
export async function disable2FA(userId: number | "me", code: string): Promise {
return await api.del({
url: `/users/${userId}/2fa`,
params: {
code,
},
});
}
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise {
return await api.post({
url: `/users/${userId}/2fa/backup-codes`,
data: { code },
});
}
================================================
FILE: frontend/src/api/backend/updateAccessList.ts
================================================
import * as api from "./base";
import type { AccessList } from "./models";
export async function updateAccessList(item: AccessList): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/nginx/access-lists/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateAuth.ts
================================================
import * as api from "./base";
import type { User } from "./models";
export async function updateAuth(userId: number | "me", newPassword: string, current?: string): Promise {
const data = {
type: "password",
current: current,
secret: newPassword,
};
if (userId === "me") {
data.current = current;
}
return await api.put({
url: `/users/${userId}/auth`,
data,
});
}
================================================
FILE: frontend/src/api/backend/updateDeadHost.ts
================================================
import * as api from "./base";
import type { DeadHost } from "./models";
export async function updateDeadHost(item: DeadHost): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/nginx/dead-hosts/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateProxyHost.ts
================================================
import * as api from "./base";
import type { ProxyHost } from "./models";
export async function updateProxyHost(item: ProxyHost): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/nginx/proxy-hosts/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateRedirectionHost.ts
================================================
import * as api from "./base";
import type { RedirectionHost } from "./models";
export async function updateRedirectionHost(item: RedirectionHost): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/nginx/redirection-hosts/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateSetting.ts
================================================
import * as api from "./base";
import type { Setting } from "./models";
export async function updateSetting(item: Setting): Promise {
// Remove readonly fields
const { id, ...data } = item;
return await api.put({
url: `/settings/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateStream.ts
================================================
import * as api from "./base";
import type { Stream } from "./models";
export async function updateStream(item: Stream): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/nginx/streams/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/updateUser.ts
================================================
import * as api from "./base";
import type { User } from "./models";
export async function updateUser(item: User): Promise {
// Remove readonly fields
const { id, createdOn: _, modifiedOn: __, ...data } = item;
return await api.put({
url: `/users/${id}`,
data: data,
});
}
================================================
FILE: frontend/src/api/backend/uploadCertificate.ts
================================================
import * as api from "./base";
import type { Certificate } from "./models";
export async function uploadCertificate(id: number, data: FormData): Promise {
return await api.post({
url: `/nginx/certificates/${id}/upload`,
data,
});
}
================================================
FILE: frontend/src/api/backend/validateCertificate.ts
================================================
import * as api from "./base";
import type { ValidatedCertificateResponse } from "./responseTypes";
export async function validateCertificate(data: FormData): Promise {
return await api.post({
url: "/nginx/certificates/validate",
data,
});
}
================================================
FILE: frontend/src/components/Button.tsx
================================================
import cn from "classnames";
import type { ReactNode } from "react";
interface Props {
children: ReactNode;
className?: string;
type?: "button" | "submit";
actionType?: "primary" | "secondary" | "success" | "warning" | "danger" | "info" | "light" | "dark";
variant?: "ghost" | "outline" | "pill" | "square" | "action";
size?: "sm" | "md" | "lg" | "xl";
fullWidth?: boolean;
isLoading?: boolean;
disabled?: boolean;
color?:
| "blue"
| "azure"
| "indigo"
| "purple"
| "pink"
| "red"
| "orange"
| "yellow"
| "lime"
| "green"
| "teal"
| "cyan";
onClick?: () => void;
}
function Button({
children,
className,
onClick,
type,
actionType,
variant,
size,
color,
fullWidth,
isLoading,
disabled,
}: Props) {
const myOnClick = () => {
!isLoading && onClick && onClick();
};
const cns = cn(
"btn",
className,
actionType && `btn-${actionType}`,
variant && `btn-${variant}`,
size && `btn-${size}`,
color && `btn-${color}`,
fullWidth && "w-100",
isLoading && "btn-loading",
);
return (
{children}
);
}
export { Button };
================================================
FILE: frontend/src/components/EmptyData.tsx
================================================
import type { Table as ReactTable } from "@tanstack/react-table";
import cn from "classnames";
import type { ReactNode } from "react";
import { Button, HasPermission } from "src/components";
import { T } from "src/locale";
import { type ADMIN, MANAGE, type Permission, type Section } from "src/modules/Permissions";
interface Props {
tableInstance: ReactTable;
onNew?: () => void;
isFiltered?: boolean;
object: string;
objects: string;
color?: string;
customAddBtn?: ReactNode;
permissionSection?: Section | typeof ADMIN;
permission?: Permission;
}
function EmptyData({
tableInstance,
onNew,
isFiltered,
object,
objects,
color = "primary",
customAddBtn,
permissionSection,
permission,
}: Props) {
return (
{isFiltered ? (
) : (
<>
{customAddBtn ? (
customAddBtn
) : (
)}
>
)}
);
}
export { EmptyData };
================================================
FILE: frontend/src/components/ErrorNotFound.tsx
================================================
import { useNavigate } from "react-router-dom";
import { Button } from "src/components";
import { T } from "src/locale";
export function ErrorNotFound() {
const navigate = useNavigate();
return (
);
}
================================================
FILE: frontend/src/components/Flag.tsx
================================================
import { IconWorld } from "@tabler/icons-react";
import { hasFlag } from "country-flag-icons";
// @ts-expect-error Creating a typing for a subfolder is not easily possible
import Flags from "country-flag-icons/react/3x2";
interface FlagProps {
className?: string;
countryCode: string;
}
function Flag({ className, countryCode }: FlagProps) {
countryCode = countryCode.toUpperCase();
if (countryCode === "EN") {
return ;
}
if (hasFlag(countryCode)) {
const FlagElement = Flags[countryCode] as any;
return ;
}
console.error(`No flag for country ${countryCode} found!`);
return null;
}
export { Flag };
================================================
FILE: frontend/src/components/Form/AccessClientFields.tsx
================================================
import { IconX } from "@tabler/icons-react";
import cn from "classnames";
import { useFormikContext } from "formik";
import { useState } from "react";
import type { AccessListClient } from "src/api/backend";
import { intl, T } from "src/locale";
interface Props {
initialValues: AccessListClient[];
name?: string;
}
export function AccessClientFields({ initialValues, name = "clients" }: Props) {
const [values, setValues] = useState(initialValues || []);
const { setFieldValue } = useFormikContext();
const blankClient: AccessListClient = { directive: "allow", address: "" };
if (values?.length === 0) {
setValues([blankClient]);
}
const handleAdd = () => {
setValues([...values, blankClient]);
};
const handleRemove = (idx: number) => {
const newValues = values.filter((_: AccessListClient, i: number) => i !== idx);
if (newValues.length === 0) {
newValues.push(blankClient);
}
setValues(newValues);
setFormField(newValues);
};
const handleChange = (idx: number, field: string, fieldValue: string) => {
const newValues = values.map((v: AccessListClient, i: number) =>
i === idx ? { ...v, [field]: fieldValue } : v,
);
setValues(newValues);
setFormField(newValues);
};
const setFormField = (newValues: AccessListClient[]) => {
const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== "");
setFieldValue(name, filtered);
};
return (
<>
{values.map((client: AccessListClient, idx: number) => (
))}
>
);
}
================================================
FILE: frontend/src/components/Form/AccessField.tsx
================================================
import { IconLock, IconLockOpen2 } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useAccessLists } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale";
interface AccessOption {
readonly value: number;
readonly label: string;
readonly subLabel: string;
readonly icon: ReactNode;
}
const Option = (props: OptionProps) => {
return (
{props.data.icon} {props.data.label}
{props.data.subLabel}
);
};
interface Props {
id?: string;
name?: string;
label?: string;
}
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
const { setFieldValue } = useFormikContext();
const handleChange = (newValue: any, _actionMeta: ActionMeta) => {
setFieldValue(name, newValue?.value);
};
const options: AccessOption[] =
data?.map((item: AccessList) => ({
value: item.id || 0,
label: item.name,
subLabel: intl.formatMessage(
{ id: "access-list.subtitle" },
{
users: item?.items?.length,
rules: item?.clients?.length,
date: item?.createdOn ? formatDateTime(item?.createdOn, locale) : "N/A",
},
),
icon: ,
})) || [];
// Public option
options?.unshift({
value: 0,
label: intl.formatMessage({ id: "access-list.public" }),
subLabel: intl.formatMessage({ id: "access-list.public.subtitle" }),
icon: ,
});
return (
{({ field, form }: any) => (
{isLoading ?
: null}
{isError ?
{`${error}`}
: null}
{!isLoading && !isError ? (
o.value === field.value) || options[0]}
options={options}
components={{ Option }}
styles={{
option: (base) => ({
...base,
height: "100%",
}),
}}
onChange={handleChange}
/>
) : null}
{form.errors[field.name] ? (
{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}
) : null}
)}
);
}
================================================
FILE: frontend/src/components/Form/BasicAuthFields.tsx
================================================
import { IconX } from "@tabler/icons-react";
import { useFormikContext } from "formik";
import { useState } from "react";
import type { AccessListItem } from "src/api/backend";
import { T } from "src/locale";
interface Props {
initialValues: AccessListItem[];
name?: string;
}
export function BasicAuthFields({ initialValues, name = "items" }: Props) {
const [values, setValues] = useState(initialValues || []);
const { setFieldValue } = useFormikContext();
const blankItem: AccessListItem = { username: "", password: "" };
if (values?.length === 0) {
setValues([blankItem]);
}
const handleAdd = () => {
setValues([...values, blankItem]);
};
const handleRemove = (idx: number) => {
const newValues = values.filter((_: AccessListItem, i: number) => i !== idx);
if (newValues.length === 0) {
newValues.push(blankItem);
}
setValues(newValues);
setFormField(newValues);
};
const handleChange = (idx: number, field: string, fieldValue: string) => {
const newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
setValues(newValues);
setFormField(newValues);
};
const setFormField = (newValues: AccessListItem[]) => {
const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== "");
setFieldValue(name, filtered);
};
return (
<>
{values.map((item: AccessListItem, idx: number) => (
))}
>
);
}
================================================
FILE: frontend/src/components/Form/DNSProviderFields.module.css
================================================
.dnsChallengeWarning {
border: 1px solid var(--tblr-orange-lt);
padding: 1rem;
border-radius: 0.375rem;
margin-top: 1rem;
background-color: var(--tblr-cyan-lt);
}
================================================
FILE: frontend/src/components/Form/DNSProviderFields.tsx
================================================
import { IconAlertTriangle } from "@tabler/icons-react";
import CodeEditor from "@uiw/react-textarea-code-editor";
import { Field, useFormikContext } from "formik";
import { useState } from "react";
import Select, { type ActionMeta } from "react-select";
import type { DNSProvider } from "src/api/backend";
import { useDnsProviders } from "src/hooks";
import { intl, T } from "src/locale";
import styles from "./DNSProviderFields.module.css";
interface DNSProviderOption {
readonly value: string;
readonly label: string;
readonly credentials: string;
}
interface Props {
showBoundaryBox?: boolean;
}
export function DNSProviderFields({ showBoundaryBox = false }: Props) {
const { values, setFieldValue } = useFormikContext();
const { data: dnsProviders, isLoading } = useDnsProviders();
const [dnsProviderId, setDnsProviderId] = useState(null);
const v: any = values || {};
const handleChange = (newValue: any, _actionMeta: ActionMeta) => {
setFieldValue("meta.dnsProvider", newValue?.value);
setFieldValue("meta.dnsProviderCredentials", newValue?.credentials);
setDnsProviderId(newValue?.value);
};
const options: DNSProviderOption[] =
dnsProviders?.map((p: DNSProvider) => ({
value: p.id,
label: p.name,
credentials: p.credentials,
})) || [];
return (
{({ field }: any) => (
)}
{dnsProviderId ? (
<>
{({ field }: any) => (
)}
{({ field }: any) => (
)}
>
) : null}
);
}
================================================
FILE: frontend/src/components/Form/DomainNamesField.tsx
================================================
import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react";
import type { ActionMeta, MultiValue } from "react-select";
import CreatableSelect from "react-select/creatable";
import { intl, T } from "src/locale";
import { validateDomain, validateDomains } from "src/modules/Validations";
type SelectOption = {
label: string;
value: string;
color?: string;
};
interface Props {
id?: string;
maxDomains?: number;
isWildcardPermitted?: boolean;
dnsProviderWildcardSupported?: boolean;
name?: string;
label?: string;
onChange?: (domains: string[]) => void;
}
export function DomainNamesField({
name = "domainNames",
label = "domain-names",
id = "domainNames",
maxDomains,
isWildcardPermitted = false,
dnsProviderWildcardSupported = false,
onChange,
}: Props) {
const { setFieldValue } = useFormikContext();
const handleChange = (v: MultiValue, _actionMeta: ActionMeta) => {
const doms = v?.map((i: SelectOption) => {
return i.value;
});
setFieldValue(name, doms);
onChange?.(doms);
};
const helperTexts: ReactNode[] = [];
if (maxDomains) {
helperTexts.push( );
}
if (!isWildcardPermitted) {
helperTexts.push( );
} else if (!dnsProviderWildcardSupported) {
helperTexts.push( );
}
return (
{({ field, form }: any) => (
({ label: d, value: d }))}
/>
{form.errors[field.name] && form.touched[field.name] ? (
{form.errors[field.name]}
) : helperTexts.length ? (
helperTexts.map((i, idx) => (
{i}
))
) : null}
)}
);
}
================================================
FILE: frontend/src/components/Form/LocationsFields.module.css
================================================
.locationCard {
border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
}
================================================
FILE: frontend/src/components/Form/LocationsFields.tsx
================================================
import { IconSettings } from "@tabler/icons-react";
import CodeEditor from "@uiw/react-textarea-code-editor";
import cn from "classnames";
import { useFormikContext } from "formik";
import { useState } from "react";
import type { ProxyLocation } from "src/api/backend";
import { intl, T } from "src/locale";
import styles from "./LocationsFields.module.css";
interface Props {
initialValues: ProxyLocation[];
name?: string;
}
export function LocationsFields({ initialValues, name = "locations" }: Props) {
const [values, setValues] = useState(initialValues || []);
const { setFieldValue } = useFormikContext();
const [advVisible, setAdvVisible] = useState([]);
const blankItem: ProxyLocation = {
path: "",
advancedConfig: "",
forwardScheme: "http",
forwardHost: "",
forwardPort: 80,
};
const toggleAdvVisible = (idx: number) => {
setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);
};
const handleAdd = () => {
setValues([...values, blankItem]);
};
const handleRemove = (idx: number) => {
const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);
setValues(newValues);
setFormField(newValues);
};
const handleChange = (idx: number, field: string, fieldValue: string) => {
const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
setValues(newValues);
setFormField(newValues);
};
const setFormField = (newValues: ProxyLocation[]) => {
const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== "");
setFieldValue(name, filtered);
};
if (values.length === 0) {
return (
);
}
return (
<>
{values.map((item: ProxyLocation, idx: number) => (
handleChange(idx, "forwardScheme", e.target.value)}
>
http
https
{advVisible.includes(idx) && (
handleChange(idx, "advancedConfig", e.target.value)}
style={{
fontFamily:
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
borderRadius: "0.3rem",
minHeight: "170px",
}}
/>
)}
))}
>
);
}
================================================
FILE: frontend/src/components/Form/NginxConfigField.tsx
================================================
import CodeEditor from "@uiw/react-textarea-code-editor";
import { Field } from "formik";
import { intl, T } from "src/locale";
interface Props {
id?: string;
name?: string;
label?: string;
}
export function NginxConfigField({
name = "advancedConfig",
label = "nginx-config.label",
id = "advancedConfig",
}: Props) {
return (
{({ field }: any) => (
)}
);
}
================================================
FILE: frontend/src/components/Form/SSLCertificateField.tsx
================================================
import { IconShield } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useCertificates } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale";
interface CertOption {
readonly value: number | "new";
readonly label: string;
readonly subLabel: string;
readonly icon: React.ReactNode;
}
const Option = (props: OptionProps) => {
return (
{props.data.icon} {props.data.label}
{props.data.subLabel}
);
};
interface Props {
id?: string;
name?: string;
label?: string;
required?: boolean;
allowNew?: boolean;
forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields
}
export function SSLCertificateField({
name = "certificateId",
label = "ssl-certificate",
id = "certificateId",
required,
allowNew,
forHttp = true,
}: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useCertificates();
const { values, setFieldValue } = useFormikContext();
const v: any = values || {};
const handleChange = (newValue: any, _actionMeta: ActionMeta) => {
setFieldValue(name, newValue?.value);
const {
sslForced,
http2Support,
hstsEnabled,
hstsSubdomains,
dnsChallenge,
dnsProvider,
dnsProviderCredentials,
propagationSeconds,
} = v;
if (forHttp && !newValue?.value) {
sslForced && setFieldValue("sslForced", false);
http2Support && setFieldValue("http2Support", false);
hstsEnabled && setFieldValue("hstsEnabled", false);
hstsSubdomains && setFieldValue("hstsSubdomains", false);
}
if (newValue?.value !== "new") {
dnsChallenge && setFieldValue("dnsChallenge", undefined);
dnsProvider && setFieldValue("dnsProvider", undefined);
dnsProviderCredentials && setFieldValue("dnsProviderCredentials", undefined);
propagationSeconds && setFieldValue("propagationSeconds", undefined);
}
};
const options: CertOption[] =
data?.map((cert: Certificate) => ({
value: cert.id,
label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn, locale) : "N/A" })}`,
icon: ,
})) || [];
// Prepend the Add New option
if (allowNew) {
options?.unshift({
value: "new",
label: intl.formatMessage({ id: "certificates.request.title" }),
subLabel: intl.formatMessage({ id: "certificates.request.subtitle" }),
icon: ,
});
}
// Prepend the None option
if (!required) {
options?.unshift({
value: 0,
label: intl.formatMessage({ id: "certificate.none.title" }),
subLabel: forHttp
? intl.formatMessage({ id: "certificate.none.subtitle.for-http" })
: intl.formatMessage({ id: "certificate.none.subtitle" }),
icon: ,
});
}
return (
{({ field, form }: any) => (
{isLoading ?
: null}
{isError ?
{`${error}`}
: null}
{!isLoading && !isError ? (
o.value === field.value) || options[0]}
options={options}
components={{ Option }}
styles={{
option: (base) => ({
...base,
height: "100%",
}),
}}
onChange={handleChange}
/>
) : null}
{form.errors[field.name] ? (
{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}
) : null}
)}
);
}
================================================
FILE: frontend/src/components/Form/SSLOptionsFields.tsx
================================================
import cn from "classnames";
import { Field, useFormikContext } from "formik";
import { DNSProviderFields, DomainNamesField } from "src/components";
import { T } from "src/locale";
interface Props {
forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields
forProxyHost?: boolean; // the advanced fields
forceDNSForNew?: boolean;
requireDomainNames?: boolean; // used for streams
color?: string;
}
export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDNSForNew, requireDomainNames, color = "bg-cyan" }: Props) {
const { values, setFieldValue } = useFormikContext();
const v: any = values || {};
const newCertificate = v?.certificateId === "new";
const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, trustForwardedProto, meta } = v;
const { dnsChallenge } = meta || {};
if (forceDNSForNew && newCertificate && !dnsChallenge) {
setFieldValue("meta.dnsChallenge", true);
}
const handleToggleChange = (e: any, fieldName: string) => {
setFieldValue(fieldName, e.target.checked);
if (fieldName === "meta.dnsChallenge" && !e.target.checked) {
setFieldValue("meta.dnsProvider", undefined);
setFieldValue("meta.dnsProviderCredentials", undefined);
setFieldValue("meta.propagationSeconds", undefined);
}
};
const toggleClasses = "form-check-input";
const toggleEnabled = cn(toggleClasses, color);
const getHttpOptions = () => (
);
const getHttpAdvancedOptions = () =>(
);
return (
{forHttp ? getHttpOptions() : null}
{newCertificate ? (
<>
{({ field }: any) => (
handleToggleChange(e, field.name)}
/>
)}
{requireDomainNames ? : null}
{dnsChallenge ? : null}
>
) : null}
{forProxyHost && forHttp ? getHttpAdvancedOptions() : null}
);
}
================================================
FILE: frontend/src/components/Form/index.ts
================================================
export * from "./AccessClientFields";
export * from "./AccessField";
export * from "./BasicAuthFields";
export * from "./DNSProviderFields";
export * from "./DomainNamesField";
export * from "./LocationsFields";
export * from "./NginxConfigField";
export * from "./SSLCertificateField";
export * from "./SSLOptionsFields";
================================================
FILE: frontend/src/components/HasPermission.tsx
================================================
import type { ReactNode } from "react";
import Alert from "react-bootstrap/Alert";
import { Loading, LoadingPage } from "src/components";
import { useUser } from "src/hooks";
import { T } from "src/locale";
import { type ADMIN, hasPermission, type Permission, type Section } from "src/modules/Permissions";
interface Props {
section?: Section | typeof ADMIN;
permission: Permission;
hideError?: boolean;
children?: ReactNode;
pageLoading?: boolean;
loadingNoLogo?: boolean;
}
function HasPermission({
section,
permission,
children,
hideError = false,
pageLoading = false,
loadingNoLogo = false,
}: Props) {
const { data, isLoading } = useUser("me");
if (!section) {
return <>{children}>;
}
if (isLoading) {
if (hideError) {
return null;
}
if (pageLoading) {
return ;
}
return ;
}
const allowed = hasPermission(section, permission, data?.permissions, data?.roles);
if (allowed) {
return <>{children}>;
}
return !hideError ? (
) : null;
}
export { HasPermission };
================================================
FILE: frontend/src/components/Loading.module.css
================================================
.logo {
max-height: 100px;
}
================================================
FILE: frontend/src/components/Loading.tsx
================================================
import type { ReactNode } from "react";
import { T } from "src/locale";
import styles from "./Loading.module.css";
interface Props {
label?: string | ReactNode;
noLogo?: boolean;
}
export function Loading({ label, noLogo }: Props) {
return (
{noLogo ? null : (
)}
{label || }
);
}
================================================
FILE: frontend/src/components/LoadingPage.tsx
================================================
import { Loading, Page } from "src/components";
interface Props {
label?: string;
noLogo?: boolean;
}
export function LoadingPage({ label, noLogo }: Props) {
return (
);
}
================================================
FILE: frontend/src/components/LocalePicker.module.css
================================================
.btn {
color: light-dark(var(--tblr-dark), var(--tblr-light)) !important;
&:hover {
border: var(--tblr-btn-border-width) solid transparent !important;
background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;
}
}
================================================
FILE: frontend/src/components/LocalePicker.tsx
================================================
import cn from "classnames";
import { Flag } from "src/components";
import { useLocaleState } from "src/context";
import { useTheme } from "src/hooks";
import { changeLocale, getFlagCodeForLocale, localeOptions, T } from "src/locale";
import styles from "./LocalePicker.module.css";
interface Props {
menuAlign?: "start" | "end";
}
function LocalePicker({ menuAlign = "start" }: Props) {
const { locale, setLocale } = useLocaleState();
const { getTheme } = useTheme();
const changeTo = (lang: string) => {
changeLocale(lang);
setLocale(lang);
location.reload();
};
const classes = ["btn", "dropdown-toggle", "btn-sm", styles.btn];
const cns = cn(...classes, getTheme() === "dark" ? "btn-ghost-dark" : "btn-ghost-light");
return (
);
}
export { LocalePicker };
================================================
FILE: frontend/src/components/NavLink.tsx
================================================
import { useNavigate } from "react-router-dom";
interface Props {
children: React.ReactNode;
to?: string;
isDropdownItem?: boolean;
onClick?: () => void;
}
export function NavLink({ children, to, isDropdownItem, onClick }: Props) {
const navigate = useNavigate();
return (
{
e.preventDefault();
if (onClick) {
onClick();
}
if (to) {
navigate(to);
}
}}
>
{children}
);
}
================================================
FILE: frontend/src/components/Page.module.css
================================================
.page {
display: grid;
grid-template-rows: auto 1fr auto; /* Header, Main Content, Footer */
min-height: 100vh;
}
================================================
FILE: frontend/src/components/Page.tsx
================================================
import cn from "classnames";
import styles from "./Page.module.css";
interface Props {
children: React.ReactNode;
className?: string;
}
export function Page({ children, className }: Props) {
return {children}
;
}
================================================
FILE: frontend/src/components/SiteContainer.tsx
================================================
interface Props {
children: React.ReactNode;
}
export function SiteContainer({ children }: Props) {
return {children}
;
}
================================================
FILE: frontend/src/components/SiteFooter.tsx
================================================
import { useCheckVersion, useHealth } from "src/hooks";
import { T } from "src/locale";
export function SiteFooter() {
const health = useHealth();
const { data: versionData } = useCheckVersion();
const getVersion = () => {
if (!health.data) {
return "";
}
const v = health.data.version;
return `v${v.major}.${v.minor}.${v.revision}`;
};
return (
);
}
================================================
FILE: frontend/src/components/SiteHeader.module.css
================================================
.logo {
font-size: 1.1rem;
font-weight: 500;
img {
margin-right: 0.8rem;
}
}
================================================
FILE: frontend/src/components/SiteHeader.tsx
================================================
import { IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react";
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context";
import { useUser } from "src/hooks";
import { T } from "src/locale";
import { showChangePasswordModal, showTwoFactorModal, showUserModal } from "src/modals";
import styles from "./SiteHeader.module.css";
export function SiteHeader() {
const { data: currentUser } = useUser("me");
const isAdmin = currentUser?.roles.includes("admin");
const { logout } = useAuthState();
return (
);
}
================================================
FILE: frontend/src/components/SiteMenu.tsx
================================================
import {
IconBook,
IconDeviceDesktop,
IconHome,
IconLock,
IconSettings,
IconShield,
IconUser,
} from "@tabler/icons-react";
import cn from "classnames";
import React from "react";
import { HasPermission, NavLink } from "src/components";
import { T } from "src/locale";
import {
ACCESS_LISTS,
ADMIN,
CERTIFICATES,
DEAD_HOSTS,
type MANAGE,
PROXY_HOSTS,
REDIRECTION_HOSTS,
type Section,
STREAMS,
VIEW,
} from "src/modules/Permissions";
interface MenuItem {
label: string;
icon?: React.ElementType;
to?: string;
items?: MenuItem[];
permissionSection?: Section | typeof ADMIN;
permission?: typeof VIEW | typeof MANAGE;
}
const menuItems: MenuItem[] = [
{
to: "/",
icon: IconHome,
label: "dashboard",
},
{
icon: IconDeviceDesktop,
label: "hosts",
items: [
{
to: "/nginx/proxy",
label: "proxy-hosts",
permissionSection: PROXY_HOSTS,
permission: VIEW,
},
{
to: "/nginx/redirection",
label: "redirection-hosts",
permissionSection: REDIRECTION_HOSTS,
permission: VIEW,
},
{
to: "/nginx/stream",
label: "streams",
permissionSection: STREAMS,
permission: VIEW,
},
{
to: "/nginx/404",
label: "dead-hosts",
permissionSection: DEAD_HOSTS,
permission: VIEW,
},
],
},
{
to: "/access",
icon: IconLock,
label: "access-lists",
permissionSection: ACCESS_LISTS,
permission: VIEW,
},
{
to: "/certificates",
icon: IconShield,
label: "certificates",
permissionSection: CERTIFICATES,
permission: VIEW,
},
{
to: "/users",
icon: IconUser,
label: "users",
permissionSection: ADMIN,
},
{
to: "/audit-log",
icon: IconBook,
label: "auditlogs",
permissionSection: ADMIN,
},
{
to: "/settings",
icon: IconSettings,
label: "settings",
permissionSection: ADMIN,
},
];
const getMenuItem = (item: MenuItem, onClick?: () => void) => {
if (item.items && item.items.length > 0) {
return getMenuDropown(item, onClick);
}
return (
{item.icon && React.createElement(item.icon, { height: 24, width: 24 })}
);
};
const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
const cns = cn("nav-item", "dropdown");
return (
{item.items?.map((subitem, idx) => {
return (
);
})}
);
};
export function SiteMenu() {
const closeMenu = () => setTimeout(() => {
const navbarToggler = document.querySelector(".navbar-toggler");
const navbarMenu = document.querySelector("#navbar-menu");
if (navbarToggler && navbarMenu?.classList.contains("show")) {
navbarToggler.click();
}
}, 300);
return (
);
}
================================================
FILE: frontend/src/components/Table/EmptyRow.tsx
================================================
import type { Table as ReactTable } from "@tanstack/react-table";
interface Props {
tableInstance: ReactTable;
}
function EmptyRow({ tableInstance }: Props) {
return (
There are no items
);
}
export { EmptyRow };
================================================
FILE: frontend/src/components/Table/Formatter/AccessListformatter.tsx
================================================
import type { AccessList } from "src/api/backend";
import { T } from "src/locale";
import { showAccessListModal } from "src/modals";
interface Props {
access?: AccessList;
}
export function AccessListFormatter({ access }: Props) {
if (!access) {
return ;
}
return (
{
e.preventDefault();
showAccessListModal(access?.id || 0);
}}
>
{access.name}
);
}
================================================
FILE: frontend/src/components/Table/Formatter/CertificateFormatter.tsx
================================================
import type { Certificate } from "src/api/backend";
import { T } from "src/locale";
interface Props {
certificate?: Certificate;
}
export function CertificateFormatter({ certificate }: Props) {
let translation = "http-only";
if (certificate) {
translation = certificate.provider;
if (translation === "letsencrypt") {
translation = "lets-encrypt";
} else if (translation === "other") {
translation = "certificates.custom";
}
}
return ;
}
================================================
FILE: frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx
================================================
import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Popover from "react-bootstrap/Popover";
import type { DeadHost, ProxyHost, RedirectionHost, Stream } from "src/api/backend";
import { TrueFalseFormatter } from "src/components";
import { T } from "src/locale";
const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => {
if (items.length === 0) {
return null;
}
return (
<>
{items.map((host) => (
{host.domainNames.join(", ")}
))}
>
);
};
const getSectionStream = (items: Stream[]) => {
if (items.length === 0) {
return null;
}
return (
<>
{items.map((stream) => (
{stream.forwardingHost}:{stream.forwardingPort}
))}
>
);
};
interface Props {
proxyHosts: ProxyHost[];
redirectionHosts: RedirectionHost[];
deadHosts: DeadHost[];
streams: Stream[];
}
export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }: Props) {
const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length + streams?.length;
if (totalCount === 0) {
return ;
}
proxyHosts.sort();
redirectionHosts.sort();
deadHosts.sort();
streams.sort();
const popover = (
{getSection("proxy-hosts", proxyHosts)}
{getSection("redirection-hosts", redirectionHosts)}
{getSection("dead-hosts", deadHosts)}
{getSectionStream(streams)}
);
return (
);
}
================================================
FILE: frontend/src/components/Table/Formatter/DateFormatter.tsx
================================================
import cn from "classnames";
import { differenceInDays, isPast } from "date-fns";
import { useLocaleState } from "src/context";
import { formatDateTime, parseDate } from "src/locale";
interface Props {
value: string;
highlightPast?: boolean;
highlistNearlyExpired?: boolean;
}
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
const { locale } = useLocaleState();
const d = parseDate(value);
const dateIsPast = d ? isPast(d) : false;
const days = d ? differenceInDays(d, new Date()) : 0;
const cl = cn({
"text-danger": highlightPast && dateIsPast,
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
});
return {formatDateTime(value, locale)} ;
}
================================================
FILE: frontend/src/components/Table/Formatter/DomainsFormatter.tsx
================================================
import cn from "classnames";
import type { ReactNode } from "react";
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale";
interface Props {
domains: string[];
createdOn?: string;
niceName?: string;
provider?: string;
color?: string;
}
const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
// when domain contains a wildcard, make the link go nowhere.
// Apparently the domain can be null or undefined sometimes.
// This try is just a safeguard to prevent the whole formatter from breaking.
if (!domain) return null;
try {
let onClick: ((e: React.MouseEvent) => void) | undefined;
if (domain.includes("*")) {
onClick = (e: React.MouseEvent) => e.preventDefault();
}
return (
{domain}
);
} catch {
return null;
}
};
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
const { locale } = useLocaleState();
const elms: ReactNode[] = [];
if ((!domains || domains.length === 0) && !niceName) {
elms.push(
Unknown
,
);
}
if (!domains || (niceName && provider !== "letsencrypt")) {
elms.push(
{niceName}
,
);
}
if (domains) {
domains.map((domain: string) => elms.push( ));
}
return (
{...elms}
{createdOn ? (
) : null}
);
}
================================================
FILE: frontend/src/components/Table/Formatter/EmailFormatter.tsx
================================================
interface Props {
email: string;
}
export function EmailFormatter({ email }: Props) {
return (
{email}
);
}
================================================
FILE: frontend/src/components/Table/Formatter/EventFormatter.tsx
================================================
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
import cn from "classnames";
import type { AuditLog } from "src/api/backend";
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale";
const getEventValue = (event: AuditLog) => {
switch (event.objectType) {
case "access-list":
case "user":
return event.meta?.name;
case "proxy-host":
case "redirection-host":
case "dead-host":
return event.meta?.domainNames?.join(", ") || "N/A";
case "stream":
return event.meta?.incomingPort || "N/A";
case "certificate":
return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A";
default:
return `UNKNOWN EVENT TYPE: ${event.objectType}`;
}
};
const getColorForAction = (action: string) => {
switch (action) {
case "created":
return "text-lime";
case "deleted":
return "text-red";
default:
return "text-blue";
}
};
const getIcon = (row: AuditLog) => {
const c = cn(getColorForAction(row.action), "me-1");
let ico = null;
switch (row.objectType) {
case "user":
ico = ;
break;
case "proxy-host":
ico = ;
break;
case "redirection-host":
ico = ;
break;
case "dead-host":
ico = ;
break;
case "stream":
ico = ;
break;
case "access-list":
ico = ;
break;
case "certificate":
ico = ;
break;
}
return ico;
};
interface Props {
row: AuditLog;
}
export function EventFormatter({ row }: Props) {
const { locale } = useLocaleState();
return (
{getIcon(row)}
— {getEventValue(row)}
{formatDateTime(row.createdOn, locale)}
);
}
================================================
FILE: frontend/src/components/Table/Formatter/GravatarFormatter.tsx
================================================
const defaultImg = "/images/default-avatar.jpg";
interface Props {
url?: string;
name?: string;
}
export function GravatarFormatter({ url, name }: Props) {
return (
);
}
================================================
FILE: frontend/src/components/Table/Formatter/RolesFormatter.tsx
================================================
import { T } from "src/locale";
interface Props {
roles: string[];
}
export function RolesFormatter({ roles }: Props) {
const r = roles || [];
if (r.length === 0) {
r[0] = "standard-user";
}
return (
<>
{r.map((role: string) => (
))}
>
);
}
================================================
FILE: frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx
================================================
import cn from "classnames";
import { T } from "src/locale";
interface Props {
value: boolean;
trueLabel?: string;
trueColor?: string;
falseLabel?: string;
falseColor?: string;
}
export function TrueFalseFormatter({
value,
trueLabel = "enabled",
trueColor = "lime",
falseLabel = "disabled",
falseColor = "red",
}: Props) {
return (
);
}
================================================
FILE: frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx
================================================
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale";
interface Props {
value: string;
createdOn?: string;
disabled?: boolean;
}
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
const { locale } = useLocaleState();
return (
);
}
================================================
FILE: frontend/src/components/Table/Formatter/index.ts
================================================
export * from "./AccessListformatter";
export * from "./CertificateFormatter";
export * from "./CertificateInUseFormatter";
export * from "./DateFormatter";
export * from "./DomainsFormatter";
export * from "./EmailFormatter";
export * from "./EventFormatter";
export * from "./GravatarFormatter";
export * from "./RolesFormatter";
export * from "./TrueFalseFormatter";
export * from "./ValueWithDateFormatter";
================================================
FILE: frontend/src/components/Table/TableBody.tsx
================================================
import { flexRender } from "@tanstack/react-table";
import type { TableLayoutProps } from "src/components";
import { EmptyRow } from "./EmptyRow";
function TableBody(props: TableLayoutProps) {
const { tableInstance, extraStyles, emptyState } = props;
const rows = tableInstance.getRowModel().rows;
if (rows.length === 0) {
return (
{emptyState ? emptyState : }
);
}
return (
{rows.map((row: any) => {
return (
{row.getVisibleCells().map((cell: any) => {
const { className } = (cell.column.columnDef.meta as any) ?? {};
return (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
);
})}
);
})}
);
}
export { TableBody };
================================================
FILE: frontend/src/components/Table/TableHeader.tsx
================================================
import type { TableLayoutProps } from "src/components";
function TableHeader(props: TableLayoutProps) {
const { tableInstance } = props;
const headerGroups = tableInstance.getHeaderGroups();
return (
{headerGroups.map((headerGroup: any) => (
{headerGroup.headers.map((header: any) => {
const { column } = header;
const { className } = (column.columnDef.meta as any) ?? {};
return (
{typeof column.columnDef.header === "string" ? `${column.columnDef.header}` : null}
);
})}
))}
);
}
export { TableHeader };
================================================
FILE: frontend/src/components/Table/TableHelpers.ts
================================================
export interface TablePagination {
limit: number;
offset: number;
total: number;
}
export interface TableSortBy {
id: string;
desc: boolean;
}
export interface TableFilter {
id: string;
value: any;
}
const tableEvents = {
FILTERS_CHANGED: "FILTERS_CHANGED",
PAGE_CHANGED: "PAGE_CHANGED",
PAGE_SIZE_CHANGED: "PAGE_SIZE_CHANGED",
TOTAL_COUNT_CHANGED: "TOTAL_COUNT_CHANGED",
SORT_CHANGED: "SORT_CHANGED",
};
const tableEventReducer = (state: any, { type, payload }: any) => {
let offset = state.offset;
switch (type) {
case tableEvents.PAGE_CHANGED:
return {
...state,
offset: payload * state.limit,
};
case tableEvents.PAGE_SIZE_CHANGED:
return {
...state,
limit: payload,
};
case tableEvents.TOTAL_COUNT_CHANGED:
return {
...state,
total: payload,
};
case tableEvents.SORT_CHANGED:
return {
...state,
sortBy: payload,
};
case tableEvents.FILTERS_CHANGED:
if (state.filters !== payload) {
// this actually was a legit change
// sets to page 1 when filter is modified
offset = 0;
}
return {
...state,
filters: payload,
offset,
};
default:
throw new Error(`Unhandled action type: ${type}`);
}
};
export { tableEvents, tableEventReducer };
================================================
FILE: frontend/src/components/Table/TableLayout.tsx
================================================
import type { Table as ReactTable } from "@tanstack/react-table";
import { TableBody } from "./TableBody";
import { TableHeader } from "./TableHeader";
interface TableLayoutProps {
tableInstance: ReactTable;
emptyState?: React.ReactNode;
extraStyles?: {
row: (rowData: TFields) => any | undefined;
};
}
function TableLayout(props: TableLayoutProps) {
const hasRows = props.tableInstance.getRowModel().rows.length > 0;
return (
);
}
export { TableLayout, type TableLayoutProps };
================================================
FILE: frontend/src/components/Table/index.ts
================================================
export * from "./Formatter";
export * from "./TableHeader";
export * from "./TableHelpers";
export * from "./TableLayout";
================================================
FILE: frontend/src/components/ThemeSwitcher.module.css
================================================
.darkBtn {
color: var(--tblr-light) !important;
&:hover {
border: var(--tblr-btn-border-width) solid transparent !important;
background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;
}
}
.lightBtn {
color: var(--tblr-dark) !important;
&:hover {
border: var(--tblr-btn-border-width) solid transparent !important;
background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important;
}
}
================================================
FILE: frontend/src/components/ThemeSwitcher.tsx
================================================
import { IconMoon, IconSun } from "@tabler/icons-react";
import cn from "classnames";
import { Button } from "src/components";
import { useTheme } from "src/hooks";
import styles from "./ThemeSwitcher.module.css";
interface Props {
className?: string;
}
function ThemeSwitcher({ className }: Props) {
const { setTheme } = useTheme();
return (
setTheme("dark")}
>
setTheme("light")}
>
);
}
export { ThemeSwitcher };
================================================
FILE: frontend/src/components/Unhealthy.tsx
================================================
import { Page } from "src/components";
export function Unhealthy() {
return (
The API is not healthy.
We'll keep checking and hope to be back soon!
);
}
================================================
FILE: frontend/src/components/index.ts
================================================
export * from "./Button";
export * from "./EmptyData";
export * from "./ErrorNotFound";
export * from "./Flag";
export * from "./Form";
export * from "./HasPermission";
export * from "./Loading";
export * from "./LoadingPage";
export * from "./LocalePicker";
export * from "./NavLink";
export * from "./Page";
export * from "./SiteContainer";
export * from "./SiteFooter";
export * from "./SiteHeader";
export * from "./SiteMenu";
export * from "./Table";
export * from "./ThemeSwitcher";
export * from "./Unhealthy";
================================================
FILE: frontend/src/context/AuthContext.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import { createContext, type ReactNode, useContext, useState } from "react";
import { useIntervalWhen } from "rooks";
import {
getToken,
isTwoFactorChallenge,
loginAsUser,
refreshToken,
verify2FA,
type TokenResponse,
} from "src/api/backend";
import AuthStore from "src/modules/AuthStore";
// 2FA challenge state
export interface TwoFactorChallenge {
challengeToken: string;
}
// Context
export interface AuthContextType {
authenticated: boolean;
twoFactorChallenge: TwoFactorChallenge | null;
login: (username: string, password: string) => Promise;
verifyTwoFactor: (code: string) => Promise;
cancelTwoFactor: () => void;
loginAs: (id: number) => Promise;
logout: () => void;
token?: string;
}
const initalValue = null;
const AuthContext = createContext(initalValue);
// Provider
interface Props {
children?: ReactNode;
tokenRefreshInterval?: number;
}
function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) {
const queryClient = useQueryClient();
const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken());
const [twoFactorChallenge, setTwoFactorChallenge] = useState(null);
const handleTokenUpdate = (response: TokenResponse) => {
AuthStore.set(response);
setAuthenticated(true);
setTwoFactorChallenge(null);
};
const login = async (identity: string, secret: string) => {
const response = await getToken(identity, secret);
if (isTwoFactorChallenge(response)) {
setTwoFactorChallenge({ challengeToken: response.challengeToken });
return;
}
handleTokenUpdate(response);
};
const verifyTwoFactor = async (code: string) => {
if (!twoFactorChallenge) {
throw new Error("No 2FA challenge pending");
}
const response = await verify2FA(twoFactorChallenge.challengeToken, code);
handleTokenUpdate(response);
};
const cancelTwoFactor = () => {
setTwoFactorChallenge(null);
};
const loginAs = async (id: number) => {
const response = await loginAsUser(id);
AuthStore.add(response);
queryClient.clear();
window.location.reload();
};
const logout = () => {
if (AuthStore.count() >= 2) {
AuthStore.drop();
queryClient.clear();
window.location.reload();
return;
}
AuthStore.clear();
setAuthenticated(false);
queryClient.clear();
};
const refresh = async () => {
const response = await refreshToken();
handleTokenUpdate(response);
};
useIntervalWhen(
() => {
if (authenticated) {
refresh();
}
},
tokenRefreshInterval,
true,
);
const value = {
authenticated,
twoFactorChallenge,
login,
verifyTwoFactor,
cancelTwoFactor,
loginAs,
logout,
};
return {children} ;
}
function useAuthState() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuthState must be used within a AuthProvider");
}
return context;
}
export { AuthProvider, useAuthState };
export default AuthContext;
================================================
FILE: frontend/src/context/LocaleContext.tsx
================================================
import { createContext, type ReactNode, useContext, useState } from "react";
import { getLocale } from "src/locale";
// Context
export interface LocaleContextType {
setLocale: (locale: string) => void;
locale?: string;
}
const initalValue = null;
const LocaleContext = createContext(initalValue);
// Provider
interface Props {
children?: ReactNode;
}
function LocaleProvider({ children }: Props) {
const [locale, setLocaleValue] = useState(getLocale());
const setLocale = async (locale: string) => {
setLocaleValue(locale);
};
const value = { locale, setLocale };
return {children} ;
}
function useLocaleState() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error("useLocaleState must be used within a LocaleProvider");
}
return context;
}
export { LocaleProvider, useLocaleState };
export default LocaleContext;
================================================
FILE: frontend/src/context/ThemeContext.tsx
================================================
import type React from "react";
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
const StorageKey = "tabler-theme";
export const Light = "light";
export const Dark = "dark";
// Define theme types
export type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
getTheme: () => Theme;
}
const ThemeContext = createContext(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
const getBrowserDefault = (): Theme => {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return Dark;
}
return Light;
};
export const ThemeProvider: React.FC = ({ children }) => {
const [theme, setThemeState] = useState(() => {
// Try to read theme from localStorage or use 'light' as default
if (typeof window !== "undefined") {
const stored = localStorage.getItem(StorageKey) as Theme | null;
return stored || getBrowserDefault();
}
return getBrowserDefault();
});
useEffect(() => {
document.body.dataset.theme = theme;
document.body.classList.remove(theme === Light ? Dark : Light);
document.body.classList.add(theme);
localStorage.setItem(StorageKey, theme);
}, [theme]);
const toggleTheme = () => {
setThemeState((prev) => (prev === Light ? Dark : Light));
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
const getTheme = () => {
return theme;
};
document.documentElement.setAttribute("data-bs-theme", theme);
return {children} ;
};
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
================================================
FILE: frontend/src/context/index.ts
================================================
export * from "./AuthContext";
export * from "./LocaleContext";
export * from "./ThemeContext";
================================================
FILE: frontend/src/declarations.d.ts
================================================
declare module "*.md";
================================================
FILE: frontend/src/hooks/index.ts
================================================
export * from "./useAccessList";
export * from "./useAccessLists";
export * from "./useAuditLog";
export * from "./useAuditLogs";
export * from "./useCertificate";
export * from "./useCertificates";
export * from "./useCheckVersion";
export * from "./useDeadHost";
export * from "./useDeadHosts";
export * from "./useDnsProviders";
export * from "./useHealth";
export * from "./useHostReport";
export * from "./useProxyHost";
export * from "./useProxyHosts";
export * from "./useRedirectionHost";
export * from "./useRedirectionHosts";
export * from "./useSetting";
export * from "./useStream";
export * from "./useStreams";
export * from "./useTheme";
export * from "./useUser";
export * from "./useUsers";
================================================
FILE: frontend/src/hooks/useAccessList.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
type AccessList,
type AccessListExpansion,
createAccessList,
getAccessList,
updateAccessList,
} from "src/api/backend";
const fetchAccessList = (id: number | "new", expand: AccessListExpansion[] = ["owner"]) => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
ownerUserId: 0,
name: "",
satisfyAny: false,
passAuth: false,
meta: {},
} as AccessList);
}
return getAccessList(id, expand);
};
const useAccessList = (id: number | "new", expand?: AccessListExpansion[], options = {}) => {
return useQuery({
queryKey: ["access-list", id, expand],
queryFn: () => fetchAccessList(id, expand),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetAccessList = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: AccessList) => (values.id ? updateAccessList(values) : createAccessList(values)),
onMutate: (values: AccessList) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["access-list", values.id]);
queryClient.setQueryData(["access-list", values.id], (old: AccessList) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["access-list", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: AccessList) => {
queryClient.invalidateQueries({ queryKey: ["access-list", id] });
queryClient.invalidateQueries({ queryKey: ["access-lists"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
},
});
};
export { useAccessList, useSetAccessList };
================================================
FILE: frontend/src/hooks/useAccessLists.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type AccessList, type AccessListExpansion, getAccessLists } from "src/api/backend";
const fetchAccessLists = (expand?: AccessListExpansion[]) => {
return getAccessLists(expand);
};
const useAccessLists = (expand?: AccessListExpansion[], options = {}) => {
return useQuery({
queryKey: ["access-lists", { expand }],
queryFn: () => fetchAccessLists(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchAccessLists, useAccessLists };
================================================
FILE: frontend/src/hooks/useAuditLog.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type AuditLog, getAuditLog } from "src/api/backend";
const fetchAuditLog = (id: number) => {
return getAuditLog(id, ["user"]);
};
const useAuditLog = (id: number, options = {}) => {
return useQuery({
queryKey: ["audit-log", id],
queryFn: () => fetchAuditLog(id),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
};
export { useAuditLog };
================================================
FILE: frontend/src/hooks/useAuditLogs.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type AuditLog, type AuditLogExpansion, getAuditLogs } from "src/api/backend";
const fetchAuditLogs = (expand?: AuditLogExpansion[]) => {
return getAuditLogs(expand);
};
const useAuditLogs = (expand?: AuditLogExpansion[], options = {}) => {
return useQuery({
queryKey: ["audit-logs", { expand }],
queryFn: () => fetchAuditLogs(expand),
staleTime: 10 * 1000,
...options,
});
};
export { fetchAuditLogs, useAuditLogs };
================================================
FILE: frontend/src/hooks/useCertificate.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type Certificate, getCertificate } from "src/api/backend";
const fetchCertificate = (id: number) => {
return getCertificate(id, ["owner"]);
};
const useCertificate = (id: number, options = {}) => {
return useQuery({
queryKey: ["certificate", id],
queryFn: () => fetchCertificate(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
export { useCertificate };
================================================
FILE: frontend/src/hooks/useCertificates.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type Certificate, type CertificateExpansion, getCertificates } from "src/api/backend";
const fetchCertificates = (expand?: CertificateExpansion[]) => {
return getCertificates(expand);
};
const useCertificates = (expand?: CertificateExpansion[], options = {}) => {
return useQuery({
queryKey: ["certificates", { expand }],
queryFn: () => fetchCertificates(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchCertificates, useCertificates };
================================================
FILE: frontend/src/hooks/useCheckVersion.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { checkVersion, type VersionCheckResponse } from "src/api/backend";
const fetchVersion = () => checkVersion();
const useCheckVersion = (options = {}) => {
return useQuery({
queryKey: ["version-check"],
queryFn: fetchVersion,
refetchOnWindowFocus: false,
retry: 5,
refetchInterval: 30 * 1000, // 30 seconds
staleTime: 5 * 60 * 1000, // 5 mins
...options,
});
};
export { fetchVersion, useCheckVersion };
================================================
FILE: frontend/src/hooks/useDeadHost.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createDeadHost, type DeadHost, getDeadHost, updateDeadHost } from "src/api/backend";
const fetchDeadHost = (id: number | "new") => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
ownerUserId: 0,
domainNames: [],
certificateId: 0,
sslForced: false,
advancedConfig: "",
meta: {},
http2Support: false,
enabled: true,
hstsEnabled: false,
hstsSubdomains: false,
} as DeadHost);
}
return getDeadHost(id, ["owner"]);
};
const useDeadHost = (id: number | "new", options = {}) => {
return useQuery({
queryKey: ["dead-host", id],
queryFn: () => fetchDeadHost(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetDeadHost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: DeadHost) => (values.id ? updateDeadHost(values) : createDeadHost(values)),
onMutate: (values: DeadHost) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["dead-host", values.id]);
queryClient.setQueryData(["dead-host", values.id], (old: DeadHost) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["dead-host", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: DeadHost) => {
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
},
});
};
export { useDeadHost, useSetDeadHost };
================================================
FILE: frontend/src/hooks/useDeadHosts.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type DeadHost, getDeadHosts, type HostExpansion } from "src/api/backend";
const fetchDeadHosts = (expand?: HostExpansion[]) => {
return getDeadHosts(expand);
};
const useDeadHosts = (expand?: HostExpansion[], options = {}) => {
return useQuery({
queryKey: ["dead-hosts", { expand }],
queryFn: () => fetchDeadHosts(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchDeadHosts, useDeadHosts };
================================================
FILE: frontend/src/hooks/useDnsProviders.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { type DNSProvider, getCertificateDNSProviders } from "src/api/backend";
const fetchDnsProviders = () => {
return getCertificateDNSProviders();
};
const useDnsProviders = (options = {}) => {
return useQuery({
queryKey: ["dns-providers"],
queryFn: () => fetchDnsProviders(),
staleTime: 300 * 1000,
...options,
});
};
export { fetchDnsProviders, useDnsProviders };
================================================
FILE: frontend/src/hooks/useHealth.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getHealth, type HealthResponse } from "src/api/backend";
const fetchHealth = () => getHealth();
const useHealth = (options = {}) => {
return useQuery({
queryKey: ["health"],
queryFn: fetchHealth,
refetchOnWindowFocus: false,
retry: 5,
refetchInterval: 15 * 1000, // 15 seconds
staleTime: 14 * 1000, // 14 seconds
...options,
});
};
export { fetchHealth, useHealth };
================================================
FILE: frontend/src/hooks/useHostReport.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getHostsReport } from "src/api/backend";
const fetchHostReport = () => getHostsReport();
const useHostReport = (options = {}) => {
return useQuery, Error>({
queryKey: ["host-report"],
queryFn: fetchHostReport,
refetchOnWindowFocus: false,
retry: 5,
refetchInterval: 15 * 1000, // 15 seconds
staleTime: 14 * 1000, // 14 seconds
...options,
});
};
export { fetchHostReport, useHostReport };
================================================
FILE: frontend/src/hooks/useProxyHost.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createProxyHost, getProxyHost, type ProxyHost, updateProxyHost } from "src/api/backend";
const fetchProxyHost = (id: number | "new") => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
ownerUserId: 0,
domainNames: [],
forwardHost: "",
forwardPort: 0,
accessListId: 0,
certificateId: 0,
sslForced: false,
cachingEnabled: false,
blockExploits: false,
advancedConfig: "",
meta: {},
allowWebsocketUpgrade: false,
http2Support: false,
forwardScheme: "",
enabled: true,
hstsEnabled: false,
hstsSubdomains: false,
trustForwardedProto: false,
} as ProxyHost);
}
return getProxyHost(id, ["owner"]);
};
const useProxyHost = (id: number | "new", options = {}) => {
return useQuery({
queryKey: ["proxy-host", id],
queryFn: () => fetchProxyHost(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetProxyHost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: ProxyHost) => (values.id ? updateProxyHost(values) : createProxyHost(values)),
onMutate: (values: ProxyHost) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["proxy-host", values.id]);
queryClient.setQueryData(["proxy-host", values.id], (old: ProxyHost) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["proxy-host", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: ProxyHost) => {
queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
},
});
};
export { useProxyHost, useSetProxyHost };
================================================
FILE: frontend/src/hooks/useProxyHosts.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getProxyHosts, type ProxyHost, type ProxyHostExpansion } from "src/api/backend";
const fetchProxyHosts = (expand?: ProxyHostExpansion[]) => {
return getProxyHosts(expand);
};
const useProxyHosts = (expand?: ProxyHostExpansion[], options = {}) => {
return useQuery({
queryKey: ["proxy-hosts", { expand }],
queryFn: () => fetchProxyHosts(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchProxyHosts, useProxyHosts };
================================================
FILE: frontend/src/hooks/useRedirectionHost.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createRedirectionHost,
getRedirectionHost,
type RedirectionHost,
updateRedirectionHost,
} from "src/api/backend";
const fetchRedirectionHost = (id: number | "new") => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
ownerUserId: 0,
domainNames: [],
forwardDomainName: "",
preservePath: false,
certificateId: 0,
sslForced: false,
advancedConfig: "",
meta: {},
http2Support: false,
forwardScheme: "auto",
forwardHttpCode: 301,
blockExploits: false,
enabled: true,
hstsEnabled: false,
hstsSubdomains: false,
} as RedirectionHost);
}
return getRedirectionHost(id, ["owner"]);
};
const useRedirectionHost = (id: number | "new", options = {}) => {
return useQuery({
queryKey: ["redirection-host", id],
queryFn: () => fetchRedirectionHost(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetRedirectionHost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: RedirectionHost) =>
values.id ? updateRedirectionHost(values) : createRedirectionHost(values),
onMutate: (values: RedirectionHost) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["redirection-host", values.id]);
queryClient.setQueryData(["redirection-host", values.id], (old: RedirectionHost) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["redirection-host", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: RedirectionHost) => {
queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
},
});
};
export { useRedirectionHost, useSetRedirectionHost };
================================================
FILE: frontend/src/hooks/useRedirectionHosts.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getRedirectionHosts, type HostExpansion, type RedirectionHost } from "src/api/backend";
const fetchRedirectionHosts = (expand?: HostExpansion[]) => {
return getRedirectionHosts(expand);
};
const useRedirectionHosts = (expand?: HostExpansion[], options = {}) => {
return useQuery({
queryKey: ["redirection-hosts", { expand }],
queryFn: () => fetchRedirectionHosts(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchRedirectionHosts, useRedirectionHosts };
================================================
FILE: frontend/src/hooks/useSetting.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getSetting, type Setting, updateSetting } from "src/api/backend";
const fetchSetting = (id: string) => {
return getSetting(id);
};
const useSetting = (id: string, options = {}) => {
return useQuery({
queryKey: ["setting", id],
queryFn: () => fetchSetting(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetSetting = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: Setting) => updateSetting(values),
onMutate: (values: Setting) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["setting", values.id]);
queryClient.setQueryData(["setting", values.id], (old: Setting) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["setting", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: Setting) => {
queryClient.invalidateQueries({ queryKey: ["setting", id] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
},
});
};
export { useSetting, useSetSetting };
================================================
FILE: frontend/src/hooks/useStream.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createStream, getStream, type Stream, updateStream } from "src/api/backend";
const fetchStream = (id: number | "new") => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
ownerUserId: 0,
tcpForwarding: true,
udpForwarding: false,
meta: {},
enabled: true,
certificateId: 0,
} as Stream);
}
return getStream(id, ["owner"]);
};
const useStream = (id: number | "new", options = {}) => {
return useQuery({
queryKey: ["stream", id],
queryFn: () => fetchStream(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetStream = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: Stream) => (values.id ? updateStream(values) : createStream(values)),
onMutate: (values: Stream) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["stream", values.id]);
queryClient.setQueryData(["stream", values.id], (old: Stream) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["stream", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: Stream) => {
queryClient.invalidateQueries({ queryKey: ["stream", id] });
queryClient.invalidateQueries({ queryKey: ["streams"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
},
});
};
export { useStream, useSetStream };
================================================
FILE: frontend/src/hooks/useStreams.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getStreams, type HostExpansion, type Stream } from "src/api/backend";
const fetchStreams = (expand?: HostExpansion[]) => {
return getStreams(expand);
};
const useStreams = (expand?: HostExpansion[], options = {}) => {
return useQuery({
queryKey: ["streams", { expand }],
queryFn: () => fetchStreams(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchStreams, useStreams };
================================================
FILE: frontend/src/hooks/useTheme.ts
================================================
import { Dark, Light, useTheme as useThemeContext } from "src/context";
// Simple hook wrapper for clarity and scalability
const useTheme = () => {
return useThemeContext();
};
export { useTheme, Dark, Light };
================================================
FILE: frontend/src/hooks/useUser.ts
================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createUser, getUser, type User, updateUser } from "src/api/backend";
const fetchUser = (id: number | string) => {
if (id === "new") {
return Promise.resolve({
id: 0,
createdOn: "",
modifiedOn: "",
isDisabled: false,
email: "",
name: "",
nickname: "",
roles: [],
avatar: "",
} as User);
}
return getUser(id, ["permissions"]);
};
const useUser = (id: string | number, options = {}) => {
return useQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
staleTime: 60 * 1000, // 1 minute
...options,
});
};
const useSetUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)),
onMutate: (values: User) => {
if (!values.id) {
return;
}
const previousObject = queryClient.getQueryData(["user", values.id]);
queryClient.setQueryData(["user", values.id], (old: User) => ({
...old,
...values,
}));
return () => queryClient.setQueryData(["user", values.id], previousObject);
},
onError: (_, __, rollback: any) => rollback(),
onSuccess: async ({ id }: User) => {
queryClient.invalidateQueries({ queryKey: ["user", id] });
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
},
});
};
export { useUser, useSetUser };
================================================
FILE: frontend/src/hooks/useUsers.ts
================================================
import { useQuery } from "@tanstack/react-query";
import { getUsers, type User, type UserExpansion } from "src/api/backend";
const fetchUsers = (expand?: UserExpansion[]) => {
return getUsers(expand);
};
const useUsers = (expand?: UserExpansion[], options = {}) => {
return useQuery({
queryKey: ["users", { expand }],
queryFn: () => fetchUsers(expand),
staleTime: 60 * 1000,
...options,
});
};
export { fetchUsers, useUsers };
================================================
FILE: frontend/src/locale/IntlProvider.tsx
================================================
import { createIntl, createIntlCache } from "react-intl";
import langBg from "./lang/bg.json";
import langDe from "./lang/de.json";
import langPt from "./lang/pt.json";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langEt from "./lang/et.json";
import langFr from "./lang/fr.json";
import langGa from "./lang/ga.json";
import langId from "./lang/id.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langKo from "./lang/ko.json";
import langNl from "./lang/nl.json";
import langPl from "./lang/pl.json";
import langRu from "./lang/ru.json";
import langSk from "./lang/sk.json";
import langCs from "./lang/cs.json";
import langVi from "./lang/vi.json";
import langZh from "./lang/zh.json";
import langTr from "./lang/tr.json";
import langHu from "./lang/hu.json";
import langNo from "./lang/no.json";
import langList from "./lang/lang-list.json";
// first item of each array should be the language code,
// not the country code
// Remember when adding to this list, also update check-locales.js script
const localeOptions = [
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["et", "et-EE", langEt],
["pt", "pt-PT", langPt],
["fr", "fr-FR", langFr],
["ga", "ga-IE", langGa],
["ja", "ja-JP", langJa],
["it", "it-IT", langIt],
["nl", "nl-NL", langNl],
["pl", "pl-PL", langPl],
["ru", "ru-RU", langRu],
["sk", "sk-SK", langSk],
["cs", "cs-CZ", langCs],
["vi", "vi-VN", langVi],
["zh", "zh-CN", langZh],
["ko", "ko-KR", langKo],
["bg", "bg-BG", langBg],
["id", "id-ID", langId],
["tr", "tr-TR", langTr],
["hu", "hu-HU", langHu],
["no", "no-NO", langNo],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = (locale || "en").slice(0, 2);
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
};
const getFlagCodeForLocale = (locale?: string) => {
const thisLocale = (locale || "en").slice(0, 2);
// only add to this if your flag is different from the locale code
const specialCases: Record = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
ko: "kr", // Korea
cs: "cz", // Czechia
};
if (specialCases[thisLocale]) {
return specialCases[thisLocale].toUpperCase();
}
return thisLocale.toUpperCase();
};
const getLocale = (short = false) => {
let loc = window.localStorage.getItem("locale");
if (!loc) {
loc = document.documentElement.lang;
}
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
};
const cache = createIntlCache();
const initialMessages = loadMessages(getLocale());
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
const changeLocale = (locale: string): void => {
const messages = loadMessages(locale);
intl = createIntl({ locale, messages }, cache);
window.localStorage.setItem("locale", locale);
document.documentElement.lang = locale;
};
// This is a translation component that wraps the translation in a span with a data
// attribute so devs can inspect the element to see the translation ID
const T = ({
id,
data,
tData,
}: {
id: string;
data?: Record;
tData?: Record;
}) => {
const translatedData: Record = {};
if (tData) {
// iterate over tData and translate each value
Object.entries(tData).forEach(([key, value]) => {
translatedData[key] = intl.formatMessage({ id: value });
});
}
return (
{intl.formatMessage(
{ id },
{
...data,
...translatedData,
},
)}
);
};
//console.log("L:", localeOptions);
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
================================================
FILE: frontend/src/locale/README.md
================================================
# Internationalisation support
## Before you start
It's highly recommended that you spin up a development instance of this project
on your docker capable server. It's pretty easy:
```bash
git clone https://github.com/NginxProxyManager/nginx-proxy-manager.git
cd nginx-proxy-manager
./scripts/start-dev -f
```
Then after a while, you can access http://yourserverip:3081
This stack will watch the file system for changes, especially to language files,
and reload the site you have open in the browser.
## Adding new translations
Modify the files in the `src` folder. Follow the conventions already there.
When the development stack is running, it will sort the locale lang files
for you when you save.
## After making changes
If you're NOT running the development stack, you will need to run
`yarn locale-compile` in the `frontend` folder for
the new translations to be compiled into the `lang` folder.
## Adding a whole new language
There's a fair bit you'll need to touch. Here's a list that may
not be complete by the time you're reading this:
- frontend/src/locale/src/[yourlang].json
- frontend/src/locale/src/lang-list.json
- frontend/src/locale/src/HelpDoc/[yourlang]/*
- frontend/src/locale/src/HelpDoc/index.tsx
- frontend/src/locale/IntlProvider.tsx
- frontend/check-locales.cjs
## Checking for missing translations in languages
Run `node check-locales.cjs` in this frontend folder.
================================================
FILE: frontend/src/locale/Utils.test.tsx
================================================
import { formatDateTime } from "src/locale";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
describe("DateFormatter", () => {
// Keep a reference to the real Intl to restore later
const RealIntl = global.Intl;
const desiredTimeZone = "Europe/London";
const desiredLocale = "en-GB";
beforeAll(() => {
// Ensure Node-based libs using TZ behave deterministically
try {
process.env.TZ = desiredTimeZone;
} catch {
// ignore if not available
}
// Mock Intl.DateTimeFormat so formatting is stable regardless of host
const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) {
super(desiredLocale, {
...options,
timeZone: desiredTimeZone,
});
}
} as unknown as typeof Intl.DateTimeFormat;
global.Intl = {
...RealIntl,
DateTimeFormat: MockedDateTimeFormat,
};
});
afterAll(() => {
// Restore original Intl after tests
global.Intl = RealIntl;
});
it("format date from iso date", () => {
const value = "2024-01-01T00:00:00.000Z";
const text = formatDateTime(value);
expect(text).toBe("1 Jan 2024, 12:00:00 am");
});
it("format date from unix timestamp number", () => {
const value = 1762476112;
const text = formatDateTime(value);
expect(text).toBe("7 Nov 2025, 12:41:52 am");
});
it("format date from unix timestamp string", () => {
const value = "1762476112";
const text = formatDateTime(value);
expect(text).toBe("7 Nov 2025, 12:41:52 am");
});
it("catch bad format from string", () => {
const value = "this is not a good date";
const text = formatDateTime(value);
expect(text).toBe("this is not a good date");
});
it("catch bad format from number", () => {
const value = -100;
const text = formatDateTime(value);
expect(text).toBe("-100");
});
it("catch bad format from number as string", () => {
const value = "-100";
const text = formatDateTime(value);
expect(text).toBe("-100");
});
});
================================================
FILE: frontend/src/locale/Utils.ts
================================================
import {
fromUnixTime,
type IntlFormatFormatOptions,
intlFormat,
parseISO,
} from "date-fns";
const isUnixTimestamp = (value: unknown): boolean => {
if (typeof value !== "number" && typeof value !== "string") return false;
const num = Number(value);
if (!Number.isFinite(num)) return false;
// Check plausible Unix timestamp range: from 1970 to ~year 3000
// Support both seconds and milliseconds
if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)
if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)
return false;
};
const parseDate = (value: string | number): Date | null => {
if (typeof value !== "number" && typeof value !== "string") return null;
try {
return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`);
} catch {
return null;
}
};
const formatDateTime = (value: string | number, locale = "en-US"): string => {
const d = parseDate(value);
if (!d) return `${value}`;
try {
return intlFormat(
d,
{
dateStyle: "medium",
timeStyle: "medium",
hourCycle: "h12",
} as IntlFormatFormatOptions,
{ locale },
);
} catch {
return `${value}`;
}
};
export { formatDateTime, parseDate, isUnixTimestamp };
================================================
FILE: frontend/src/locale/index.ts
================================================
export * from "./IntlProvider";
export * from "./Utils";
================================================
FILE: frontend/src/locale/scripts/locale-sort.cjs
================================================
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const DIR = path.resolve(__dirname, "../src");
// Function to sort object keys recursively
function sortKeys(obj) {
if (obj === null || typeof obj !== "object" || obj instanceof Array) {
return obj;
}
const sorted = {};
const keys = Object.keys(obj).sort();
for (const key of keys) {
const value = obj[key];
if (typeof value === "object" && value !== null && !(value instanceof Array)) {
sorted[key] = sortKeys(value);
} else {
sorted[key] = value;
}
}
return sorted;
}
// Get all JSON files in the directory
const files = fs.readdirSync(DIR).filter((file) => {
return file.endsWith(".json") && file !== "lang-list.json";
});
files.forEach((file) => {
const filePath = path.join(DIR, file);
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
return;
}
if (stats.size === 0) {
console.log(`Skipping empty file ${file}`);
return;
}
try {
// Read original content
const originalContent = fs.readFileSync(filePath, "utf8");
const originalJson = JSON.parse(originalContent);
// Sort keys
const sortedJson = sortKeys(originalJson);
// Convert back to string with tabs
const sortedContent = JSON.stringify(sortedJson, null, "\t") + "\n";
// Compare (normalize whitespace)
if (originalContent.trim() === sortedContent.trim()) {
console.log(`${file} is already sorted`);
return;
}
// Write sorted content
fs.writeFileSync(filePath, sortedContent, "utf8");
console.log(`Sorted ${file}`);
} catch (error) {
console.error(`Error processing ${file}:`, error.message);
}
});
================================================
FILE: frontend/src/locale/scripts/locale-sort.sh
================================================
#!/bin/bash
set -e -o pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR/../src" || exit 1
if ! command -v jq &> /dev/null; then
echo "jq could not be found, please install it to sort JSON files."
exit 1
fi
# iterate over all json files in the current directory
for file in *.json; do
if [[ -f "$file" ]]; then
if [[ ! -s "$file" ]]; then
echo "Skipping empty file $file"
continue
fi
if [ "$file" == "lang-list.json" ]; then
continue
fi
# get content of file before sorting
original_content=$(<"$file")
# compare with sorted content
sorted_content=$(jq --tab --sort-keys . "$file")
if [ "$original_content" == "$sorted_content" ]; then
echo "$file is already sorted"
continue
fi
echo "Sorting $file"
tmp=$(mktemp) && jq --tab --sort-keys . "$file" > "$tmp" && mv "$tmp" "$file"
fi
done
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/AccessLists.md
================================================
## Какво представлява Списъкът за достъп?
Списъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация.
Можете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_.
Това е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/Certificates.md
================================================
## Помощ за сертификати
### HTTP сертификат
HTTP валидираният сертификат означава, че сървърите на Let’s Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата.
За този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява.
Този процес _не_ поддържа wildcard домейни.
### DNS сертификат
DNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Let’s Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата.
Не е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP.
Този процес _поддържа_ wildcard домейни.
### Персонализиран сертификат
Използвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/DeadHosts.md
================================================
## Какво представлява 404 хост?
404 хост е просто конфигурация на хост, който показва страница с грешка 404.
Това може да е полезно, когато вашият домейн е индексиран в търсачките и искате
да предоставите по-приятна страница за грешка или да уведомите индексиращите системи,
че страниците на домейна вече не съществуват.
Допълнително предимство на този хост е възможността да проследявате логовете на заявките
към него и да виждате реферерите.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md
================================================
## Какво представлява Прокси хост?
Прокси хост е входна точка за уеб услуга, която искате да препращате.
Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.
Прокси хостовете са най-често използваната функция в Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md
================================================
## Какво представлява Хост за пренасочване?
Хостът за пренасочване пренасочва заявките от входящия домейн и прехвърля
потребителя към друг домейн.
Най-честата причина за използване на този тип хост е, когато вашият уебсайт
промени домейна си, но все още има линкове от търсачки или реферери, които сочат към стария домейн.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/Streams.md
================================================
## Какво представлява Потокът (Stream)?
Относително нова функция за Nginx, Потокът позволява препращане на TCP/UDP
трафик директно към друг компютър в мрежата.
Това е полезно, ако хоствате игрови сървъри, FTP или SSH сървъри.
================================================
FILE: frontend/src/locale/src/HelpDoc/bg/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/AccessLists.md
================================================
## Co je seznam přístupů?
Seznamy 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.
Můž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ů.
Toto 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/Certificates.md
================================================
## Pomoc s certifikáty
### Certifikát HTTP
Certifikát ověřený prostřednictvím protokolu HTTP znamená, že servery Let's Encrypt se
pokusí připojit k vašim doménám přes protokol HTTP (nikoli HTTPS!) a v případě úspěchu
vydají váš certifikát.
Pro tuto metodu budete muset mít pro své domény vytvořeného _Proxy Host_, který
je přístupný přes HTTP a směruje na tuto instalaci Nginx. Po vydání certifikátu
můžete změnit _Proxy Host_ tak, aby tento certifikát používal i pro HTTPS
připojení. _Proxy Host_ však bude stále potřeba nakonfigurovat pro přístup přes HTTP,
aby se certifikát mohl obnovit.
Tento proces _nepodporuje_ domény se zástupnými znaky.
### Certifikát DNS
Certifikát ověřený DNS vyžaduje použití pluginu DNS Provider. Tento DNS
Provider se použije na vytvoření dočasných záznamů ve vaší doméně a poté Let's
Encrypt ověří tyto záznamy, aby se ujistil, že jste vlastníkem, a pokud bude úspěšný,
vydá váš certifikát.
Před požádáním o tento typ certifikátu není potřeba vytvořit _Proxy Host_.
Není také potřeba mít _Proxy Host_ nakonfigurovaný pro přístup HTTP.
Tento proces _podporuje_ domény se zástupnými znaky.
### Vlastní certifikát
Tuto možnost použijte na nahrání vlastního SSL certifikátu, který vám poskytla vaše
certifikační autorita.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/DeadHosts.md
================================================
## Co je to 404 Host?
404 Host je jednoduše nastavení hostitele, které zobrazuje stránku 404.
To může být užitečné, pokud je vaše doména uvedena ve vyhledávačích a chcete
poskytnout hezčí chybovou stránku nebo konkrétně oznámit vyhledávačům, že
stránky domény již neexistují.
Další výhodou tohoto hostitele je sledování protokolů o návštěvách a
zobrazení odkazů.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/ProxyHosts.md
================================================
## Co je proxy hostitel?
Proxy hostitel je příchozí koncový bod pro webovou službu, kterou chcete přesměrovat.
Poskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL.
Proxy hostitelé jsou nejběžnějším použitím pro Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/RedirectionHosts.md
================================================
## Co je přesměrovací hostitel?
Přesměrovací hostitel přesměruje požadavky z příchozí domény a přesměruje
návštěvníka na jinou doménu.
Nejčastějším důvodem pro použití tohoto typu hostitele je situace, kdy vaše webová stránka změní
doménu, ale stále máte odkazy ve vyhledávačích nebo referenční odkazy směřující na starou doménu.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/Streams.md
================================================
## Co je stream?
Stream je relativně nová funkce pro Nginx, která slouží na přesměrování TCP/UDP
datového toku přímo do jiného počítače v síti.
Pokud provozujete herní servery, FTP nebo SSH servery, tato funkce se vám může hodit.
================================================
FILE: frontend/src/locale/src/HelpDoc/cs/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/de/AccessLists.md
================================================
## Was ist eine Zugriffsliste?
Zugriffslisten bieten eine Blacklist oder Whitelist mit bestimmten Client-IP-Adressen sowie eine Authentifizierung für die Proxy-Hosts über die grundlegende HTTP-Authentifizierung.
Sie 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.
Dies ist besonders nützlich für weitergeleitete Webdienste, die keine integrierten Authentifizierungsmechanismen haben, oder wenn Sie sich vor unbekannten Clients schützen möchten.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/Certificates.md
================================================
## Hilfe zu Zertifikaten
### HTTP-Zertifikat
Ein HTTP-validiertes Zertifikat bedeutet, dass Let's Encrypt-Server
versuchen, Ihre Domains über HTTP (nicht HTTPS!) zu erreichen, und wenn dies erfolgreich ist,
stellen sie Ihr Zertifikat aus.
Für diese Methode müssen Sie einen _Proxy-Host_ für Ihre Domain(s) erstellen, der
über HTTP zugänglich ist und auf diese Nginx-Installation verweist. Nachdem ein Zertifikat
ausgestellt wurde, können Sie den _Proxy-Host_ so ändern, dass dieses Zertifikat auch für HTTPS-Verbindungen
verwendet wird. Der _Proxy-Host_ muss jedoch weiterhin für den HTTP-Zugriff konfiguriert sein,
damit das Zertifikat erneuert werden kann.
Dieser Prozess unterstützt keine Wildcard-Domains.
### DNS-Zertifikat
Für ein DNS-validiertes Zertifikat müssen Sie ein DNS-Provider-Plugin verwenden. Dieser DNS-
Provider wird verwendet, um temporäre Einträge auf Ihrer Domain zu erstellen. Anschließend fragt Let's
Encrypt diese Einträge ab, um sicherzustellen, dass Sie der Eigentümer sind. Bei Erfolg wird
Ihr Zertifikat ausgestellt.
Sie müssen vor der Beantragung dieser Art von Zertifikat keinen _Proxy-Host_ erstellen.
Sie müssen Ihren _Proxy-Host_ auch nicht für den HTTP-Zugriff konfigurieren.
Dieser Prozess unterstützt Wildcard-Domains.
### Benutzerdefiniertes Zertifikat
Verwenden Sie diese Option, um Ihr eigenes SSL-Zertifikat hochzuladen, das Ihnen von Ihrer eigenen
Zertifizierungsstelle bereitgestellt wurde.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/DeadHosts.md
================================================
## Was ist ein 404-Host?
Ein 404-Host ist ein Host-Setup, das eine 404-Seite anzeigt.
Dies kann nützlich sein, wenn Ihre Domain in Suchmaschinen gelistet ist und Sie
eine ansprechendere Fehlerseite bereitstellen oder den Suchindexern ausdrücklich mitteilen möchten, dass
die Domain-Seiten nicht mehr existieren.
Ein weiterer Vorteil dieses Hosts besteht darin, dass Sie die Protokolle für Zugriffe darauf verfolgen und
die Verweise anzeigen können.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/ProxyHosts.md
================================================
## Was ist ein Proxy-Host?
Ein Proxy-Host ist der eingehende Endpunkt für einen Webdienst, den Sie weiterleiten möchten.
Er bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat.
Proxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/RedirectionHosts.md
================================================
## Was ist ein Redirection Host?
Ein Redirection Host leitet Anfragen von der eingehenden Domain weiter und leitet den
Besucher zu einer anderen Domain weiter.
Der häufigste Grund für die Verwendung dieses Host-Typs ist, wenn Ihre Website die
Domain wechselt, aber Sie noch Suchmaschinen- oder Referrer-Links haben, die auf die alte Domain verweisen.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/Streams.md
================================================
## Was ist ein Stream?
Ein Stream ist eine relativ neue Funktion von Nginx, die dazu dient, TCP/UDP-Datenverkehr
direkt an einen anderen Computer im Netzwerk weiterzuleiten.
Wenn Sie Spielserver, FTP- oder SSH-Server betreiben, kann dies sehr nützlich sein.
================================================
FILE: frontend/src/locale/src/HelpDoc/de/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/en/AccessLists.md
================================================
## What is an Access List?
Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.
You can configure multiple client rules, usernames and passwords for a single Access List and then apply that to one or more _Proxy Hosts_.
This is most useful for forwarded web services that do not have authentication mechanisms built in or when you want to protect from unknown clients.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/Certificates.md
================================================
## Certificates Help
### HTTP Certificate
A HTTP validated certificate means Let's Encrypt servers will
attempt to reach your domains over HTTP (not HTTPS!) and if successful, they
will issue your certificate.
For this method, you will have to have a _Proxy Host_ created for your domains(s) that
is accessible with HTTP and pointing to this Nginx installation. After a certificate
has been given, you can modify the _Proxy Host_ to also use this certificate for HTTPS
connections. However, the _Proxy Host_ will still need to be configured for HTTP access
in order for the certificate to renew.
This process _does not_ support wildcard domains.
### DNS Certificate
A DNS validated certificate requires you to use a DNS Provider plugin. This DNS
Provider will be used to create temporary records on your domain and then Let's
Encrypt will query those records to be sure you're the owner and if successful, they
will issue your certificate.
You do not need a _Proxy Host_ to be created prior to requesting this type of
certificate. Nor do you need to have your _Proxy Host_ configured for HTTP access.
This process _does_ support wildcard domains.
### Custom Certificate
Use this option to upload your own SSL Certificate, as provided by your own
Certificate Authority.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/DeadHosts.md
================================================
## What is a 404 Host?
A 404 Host is simply a host setup that shows a 404 page.
This can be useful when your domain is listed in search engines and you want
to provide a nicer error page or specifically to tell the search indexers that
the domain pages no longer exist.
Another benefit of having this host is to track the logs for hits to it and
view the referrers.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/ProxyHosts.md
================================================
## What is a Proxy Host?
A Proxy Host is the incoming endpoint for a web service that you want to forward.
It provides optional SSL termination for your service that might not have SSL support built in.
Proxy Hosts are the most common use for the Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md
================================================
## What is a Redirection Host?
A Redirection Host will redirect requests from the incoming domain and push the
viewer to another domain.
The most common reason to use this type of host is when your website changes
domains but you still have search engine or referrer links pointing to the old domain.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/Streams.md
================================================
## What is a Stream?
A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP
traffic directly to another computer on the network.
If you're running game servers, FTP or SSH servers this can come in handy.
================================================
FILE: frontend/src/locale/src/HelpDoc/en/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/es/AccessLists.md
================================================
## ¿Qué es una Lista de Acceso?
Las 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.
Puede 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_.
Esto es más útil para servicios web reenviados que no tienen mecanismos de autenticación integrados o cuando desea protegerse de clientes desconocidos.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/Certificates.md
================================================
## Ayuda de Certificados
### Certificado HTTP
Un certificado validado por HTTP significa que los servidores de Let's Encrypt
intentarán acceder a tus dominios a través de HTTP (¡no HTTPS!) y, si tienen éxito,
emitirán tu certificado.
Para este método, deberás tener un _Host Proxy_ creado para tu(s) dominio(s) que
sea accesible por HTTP y que apunte a esta instalación de Nginx. Después de que se
haya emitido un certificado, puedes modificar el _Host Proxy_ para que también use
este certificado para conexiones HTTPS. Sin embargo, el _Host Proxy_ seguirá
necesitando estar configurado para acceso HTTP para que el certificado se renueve.
Este proceso _no_ admite dominios comodín.
### Certificado DNS
Un certificado validado por DNS requiere que uses un complemento de Proveedor de DNS.
Este Proveedor de DNS se usará para crear registros temporales en tu dominio y luego
Let's Encrypt consultará esos registros para asegurarse de que eres el propietario y,
si tiene éxito, emitirá tu certificado.
No necesitas tener un _Host Proxy_ creado antes de solicitar este tipo de certificado.
Tampoco necesitas tener tu _Host Proxy_ configurado para acceso HTTP.
Este proceso _sí_ admite dominios comodín.
### Certificado Personalizado
Usa esta opción para cargar tu propio Certificado SSL, proporcionado por tu propia
Autoridad de Certificación.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/DeadHosts.md
================================================
## ¿Qué es un Host 404?
Un Host 404 es simplemente una configuración de host que muestra una página 404.
Esto puede ser útil cuando tu dominio está listado en los motores de búsqueda y deseas
proporcionar una página de error más agradable o específicamente para indicar a los indexadores de búsqueda que
las páginas del dominio ya no existen.
Otro beneficio de tener este host es rastrear los registros de visitas a él y
ver los referentes.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/ProxyHosts.md
================================================
## ¿Qué es un Host Proxy?
Un Host Proxy es el punto de entrada para un servicio web que deseas reenviar.
Proporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado.
Los Hosts Proxy son el uso más común del Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/RedirectionHosts.md
================================================
## ¿Qué es un Host de Redirección?
Un Host de Redirección redirigirá las solicitudes del dominio entrante e impulsará al
visitante a otro dominio.
La razón más común para usar este tipo de host es cuando tu sitio web cambia de
dominios pero aún tienes enlaces de motores de búsqueda o referencias apuntando al dominio anterior.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/Streams.md
================================================
## ¿Qué es un Stream?
Una característica relativamente nueva para Nginx, un Stream servirá para reenviar tráfico TCP/UDP
directamente a otra computadora en la red.
Si estás ejecutando servidores de juegos, FTP o servidores SSH esto puede ser muy útil.
================================================
FILE: frontend/src/locale/src/HelpDoc/es/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/et/AccessLists.md
================================================
## Mis on juurdepääsuloend?
Ligipääsuloendid pakuvad konkreetsete klientide IP-aadresside musta või valget nimekirja koos puhverserverite autentimisega põhilise HTTP-autentimise kaudu.
Saate ühe juurdepääsuloendi jaoks konfigureerida mitu kliendireeglit, kasutajanime ja parooli ning seejärel rakendada neid ühele või mitmele _puhverserverile_.
See on kõige kasulikum edastatud veebiteenuste puhul, millel pole sisseehitatud autentimismehhanisme või kui soovite kaitsta tundmatute klientide eest.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/Certificates.md
================================================
## Sertifikaatide abi
### HTTP-sertifikaat
HTTP-valideeritud sertifikaat tähendab, et Let's Encrypti serverid
proovivad teie domeenidega ühendust luua HTTP (mitte HTTPS!) kaudu ja kui see õnnestub,
väljastavad nad teile sertifikaadi.
Selle 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
ühenduste jaoks kasutada. Sertifikaadi uuendamiseks tuleb aga _Proxy Host_ ikkagi HTTP-juurdepääsu jaoks konfigureerida.
See protsess _ei_ toeta metamärke kasutavaid domeene.
### DNS-sertifikaat
DNS-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
Encrypt nende kirjete kohta päringu, et veenduda, et olete omanik, ja kui see õnnestub, väljastavad nad teile sertifikaadi.
Selle tüüpi sertifikaadi taotlemiseks ei ole vaja luua _Proxy Host_'i. Samuti ei pea teie _Proxy Host_ olema HTTP-juurdepääsu jaoks konfigureeritud.
See protsess _toetab_ metamärke kasutavaid domeene.
### Kohandatud sertifikaat
Kasutage seda valikut oma SSL-sertifikaadi üleslaadimiseks, mille on esitanud teie enda sertifitseerimisasutus.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/DeadHosts.md
================================================
## Mis on 404 host?
404 host on lihtsalt hosti seadistus, mis kuvab 404 lehte.
See võib olla kasulik, kui teie domeen on otsingumootorites loetletud ja soovite
esitada kenama vealehe või konkreetselt otsingu indekseerijatele öelda, et
domeenilehed enam ei eksisteeri.
Selle hosti teine eelis on selle külastatavuste logide jälgimine ja suunajate vaatamine.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/ProxyHosts.md
================================================
## Mis on puhverserver?
Puhverserver on veebiteenuse sissetuleva andmevoo lõpp-punkt, mida soovite edastada.
See pakub valikulist SSL-i lõpetamist teie teenusele, millel ei pruugi olla sisseehitatud SSL-tuge.
Puhverserverid on Nginxi puhverserveri halduri kõige levinum kasutusala.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/RedirectionHosts.md
================================================
## Mis on ümbersuunamishost?
Ümbersuunamishost suunab sissetuleva domeeni päringud ümber ja suunab vaataja teisele domeenile.
Kõ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.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/Streams.md
================================================
## Mis on voog?
Nginxi suhteliselt uus funktsioon, voog, edastab TCP/UDP liiklust otse võrgus olevale teisele arvutile.
Kui sul on mänguserverid, FTP- või SSH-serverid, võib see kasuks tulla.
================================================
FILE: frontend/src/locale/src/HelpDoc/et/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/AccessLists.md
================================================
## Qu'est-ce qu'une liste d'accès ?
Les 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.
Vous 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.
Ceci 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/Certificates.md
================================================
## Aide concernant les certificats
### Certificat HTTP
Un 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.
Pour 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é.
Ce processus ne prend pas en charge les domaines génériques.
### Certificat DNS
Un 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.
Il n'est pas nécessaire de créer un Hôte Proxy avant de demander ce type de certificat.
Il n'est pas non plus nécessaire de configurer votre Hôte Proxy pour l'accès HTTP.
Ce processus prend en charge les domaines génériques.
## Certificat personnalisé
Utilisez cette option pour importer votre propre certificat SSL, fourni par votre autorité de certification.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/DeadHosts.md
================================================
## Qu'est-ce qu'un serveur 404 ?
Un Hôte 404 est simplement un hôte configuré pour afficher une page 404.
Cela 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.
Un autre avantage de cet hôte est la possibilité de suivre les journaux et de consulter les sites référenceurs.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/ProxyHosts.md
================================================
## Qu'est-ce qu'un hôte proxy ?
Un Hôte Proxy est le point de terminaison entrant d'un service web que vous souhaitez rediriger.
Il assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement.
Les Hôtes Proxy constituent l'utilisation la plus courante du Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/RedirectionHosts.md
================================================
## Qu'est-ce qu'un serveur de redirection ?
Un Hôte de Redirection redirige les requêtes provenant du domaine entrant vers un autre domaine.
On 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/Streams.md
================================================
## Qu'est-ce qu'un Stream ?
Fonctionnalité relativement récente de Nginx, un Stream permet de rediriger le trafic TCP/UDP directement vers un autre ordinateur du réseau.
Si vous gérez des serveurs de jeux, FTP ou SSH, cela peut s'avérer très utile.
================================================
FILE: frontend/src/locale/src/HelpDoc/fr/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/AccessLists.md
================================================
## Cad is Liosta Rochtana ann?
Solá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.
Is 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ó.
Tá 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/Certificates.md
================================================
## Cabhair le Deimhnithe
### Teastas HTTP
Ciallaí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.
Chun 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í thacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas DNS
É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.
Ní 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.
_Tacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas Saincheaptha
Ú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 é.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/DeadHosts.md
================================================
## Cad is Óstach 404 ann?
Is socrú óstach a thaispeánann leathanach 404 é Óstach 404.
Is 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.
Buntáiste eile a bhaineann leis an óstach seo a bheith agat ná go bhfeictear na logaí le haghaidh amas agus go bhfeictear na tagairtí.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md
================================================
## Cad is Óstach Seachfhreastalaí ann?
Is é Óstach Seachfhreastalaí an críochphointe isteach do sheirbhís ghréasáin ar mhaith leat a atreorú.
Soláthraíonn sé foirceannadh SSL roghnach do do sheirbhís nach bhfuil tacaíocht SSL ionsuite inti b'fhéidir.
Is iad Óstaigh Seachfhreastalaí an úsáid is coitianta a bhaintear as Bainisteoir Seachfhreastalaí Nginx.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md
================================================
## Cad is Óstach Athsheolta ann?
Déanfaidh Óstach Athsheolta iarratais a atreorú ón bhfearann ag teacht isteach agus an breathnóir a bhrú chuig fearann eile.
Is é 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/Streams.md
================================================
## Cad is Sruth ann?
Gné 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.
Más freastalaithe cluichí, freastalaithe FTP nó SSH atá á rith agat, d’fhéadfadh sé seo a bheith úsáideach.
================================================
FILE: frontend/src/locale/src/HelpDoc/ga/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/AccessLists.md
================================================
## Mi az a hozzáférési lista?
A 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.
Egyetlen 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.
Ez 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/Certificates.md
================================================
## Tanúsítványok súgó
### HTTP tanúsítvány
A 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.
Ehhez 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.
Ez a folyamat _nem_ támogatja a helyettesítő karakteres domaineket.
### DNS tanúsítvány
A 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.
Nem 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.
Ez a folyamat _támogatja_ a helyettesítő karakteres domaineket.
### Egyéni tanúsítvány
Ezt 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/DeadHosts.md
================================================
## Mi az a 404-es Kiszolgáló?
A 404-es Kiszolgáló egyszerűen egy olyan kiszolgáló beállítás, amely egy 404-es oldalt jelenít meg.
Ez 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.
Ennek 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/ProxyHosts.md
================================================
## Mi az a Proxy Kiszolgáló?
A Proxy Kiszolgáló egy bejövő végpont egy olyan webszolgáltatáshoz, amelyet továbbítani szeretnél.
Opcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás.
A Proxy Kiszolgálók az Nginx Proxy Manager leggyakoribb felhasználási módjai.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/RedirectionHosts.md
================================================
## Mi az az Átirányító Kiszolgáló?
Az Á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.
Ennek 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/Streams.md
================================================
## Mi az a Stream?
Az 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.
Ha játékszervereket, FTP vagy SSH szervereket futtatsz, ez hasznos lehet.
================================================
FILE: frontend/src/locale/src/HelpDoc/hu/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/id/AccessLists.md
================================================
## Apa itu Daftar Akses?
Daftar Akses menyediakan daftar hitam atau daftar putih alamat IP klien tertentu beserta autentikasi untuk Host Proxy melalui Autentikasi HTTP Basic.
Anda dapat mengonfigurasi beberapa aturan klien, nama pengguna, dan kata sandi untuk satu Daftar Akses lalu menerapkannya ke satu atau lebih _Host Proxy_.
Ini paling berguna untuk layanan web yang diteruskan yang tidak memiliki mekanisme autentikasi bawaan atau ketika Anda ingin melindungi dari klien yang tidak dikenal.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/Certificates.md
================================================
## Bantuan Sertifikat
### Sertifikat HTTP
Sertifikat yang divalidasi HTTP berarti server Let's Encrypt akan
mencoba menjangkau domain Anda melalui HTTP (bukan HTTPS!) dan jika berhasil, mereka
akan menerbitkan sertifikat Anda.
Untuk metode ini, Anda harus membuat _Host Proxy_ untuk domain Anda yang
dapat diakses dengan HTTP dan mengarah ke instalasi Nginx ini. Setelah sertifikat
diberikan, Anda dapat mengubah _Host Proxy_ agar juga menggunakan sertifikat ini untuk HTTPS
koneksi. Namun, _Host Proxy_ tetap perlu dikonfigurasi untuk akses HTTP
agar sertifikat dapat diperpanjang.
Proses ini _tidak_ mendukung domain wildcard.
### Sertifikat DNS
Sertifikat yang divalidasi DNS mengharuskan Anda menggunakan plugin Penyedia DNS. Penyedia DNS ini
akan digunakan untuk membuat record sementara pada domain Anda dan kemudian Let's
Encrypt akan menanyakan record tersebut untuk memastikan Anda pemiliknya dan jika berhasil, mereka
akan menerbitkan sertifikat Anda.
Anda tidak perlu membuat _Host Proxy_ sebelum meminta jenis sertifikat ini.
Anda juga tidak perlu mengonfigurasi _Host Proxy_ untuk akses HTTP.
Proses ini _mendukung_ domain wildcard.
### Sertifikat Kustom
Gunakan opsi ini untuk mengunggah Sertifikat SSL Anda sendiri, sebagaimana disediakan oleh
Certificate Authority Anda.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/DeadHosts.md
================================================
## Apa itu Host 404?
Host 404 adalah konfigurasi host yang menampilkan halaman 404.
Ini dapat berguna ketika domain Anda terindeks di mesin pencari dan Anda ingin
menyediakan halaman error yang lebih baik atau secara khusus memberi tahu pengindeks pencarian bahwa
halaman domain tersebut sudah tidak ada.
Manfaat lain memiliki host ini adalah melacak log untuk akses ke host tersebut dan
melihat perujuk.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/ProxyHosts.md
================================================
## Apa itu Host Proxy?
Host Proxy adalah endpoint masuk untuk layanan web yang ingin Anda teruskan.
Host ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan.
Host Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md
================================================
## Apa itu Host Pengalihan?
Host Pengalihan akan mengalihkan permintaan dari domain masuk dan mengarahkan pengunjung ke domain lain.
Alasan 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/Streams.md
================================================
## Apa itu Stream?
Fitur yang relatif baru untuk Nginx, Stream berfungsi untuk meneruskan trafik TCP/UDP
langsung ke komputer lain di jaringan.
Jika Anda menjalankan server game, FTP, atau SSH, ini bisa sangat membantu.
================================================
FILE: frontend/src/locale/src/HelpDoc/id/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/index.ts
================================================
import * as bg from "./bg/index";
import * as de from "./de/index";
import * as pt from "./pt/index";
import * as en from "./en/index";
import * as es from "./es/index";
import * as et from "./et/index";
import * as fr from "./fr/index";
import * as ga from "./ga/index";
import * as id from "./id/index";
import * as it from "./it/index";
import * as ja from "./ja/index";
import * as ko from "./ko/index";
import * as nl from "./nl/index";
import * as pl from "./pl/index";
import * as ru from "./ru/index";
import * as sk from "./sk/index";
import * as cs from "./cs/index";
import * as vi from "./vi/index";
import * as zh from "./zh/index";
import * as tr from "./tr/index";
import * as hu from "./hu/index";
const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu };
const fallbackLang = "en";
export const getHelpFile = (lang: string, section: string): string => {
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
return items[lang][section].default;
}
// Fallback to English
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
return items[fallbackLang][section].default;
}
throw new Error(`Cannot load help doc for ${lang}-${section}`);
};
export default items;
================================================
FILE: frontend/src/locale/src/HelpDoc/it/AccessLists.md
================================================
## Che cos'è una Lista di Accesso?
La 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.
È possibile configurare più regole client, nomi utente e password per un singolo lista di accesso e quindi applicarlo a uno o più host proxy.
Ciò è particolarmente utile per i servizi web inoltrati che non dispongono di meccanismi di autenticazione integrati o quando si desidera proteggersi da client sconosciuti.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/Certificates.md
================================================
## Guida sui Certificati
### Certificato HTTP
Un 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.
Per questo metodo, dovrai creare un _Proxy Host_ per i tuoi domini chesia accessibile con HTTP e che punti a questa installazione Nginx.
Dopo che il certificato è stato rilasciato, puoi modificare il _Proxy Host_ per utilizzare questo certificato anche per le connessioni HTTPS.
Tuttavia, il _Proxy Host_ dovrà comunque essere configurato per l'accesso HTTP affinché il certificato possa essere rinnovato.
Questo processo _non_ supporta i domini wildcard.
### Certificato DNS
Un certificato convalidato dal DNS richiede l'uso di un plugin DNS Provider. Questo DNS Provider verrà utilizzato per creare record temporanei sul tuo dominio,
quindi Let's Encrypt interrogherà tali record per assicurarsi che tu sia il proprietario e, in caso di esito positivo,rilascerà il tuo certificato.
Non è necessario creare un _Proxy Host_ prima di richiedere questo tipo di certificato. Non è nemmeno necessario configurare il tuo _proxy host_ per l'accesso HTTP.
Questo processo _supporta_ i domini wildcard.
### Certificato personalizzato
Utilizza questa opzione per caricare il tuo certificato SSL, fornito dalla tua autorità di certificazione.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/DeadHosts.md
================================================
## Che cos'è un Host 404?
Un Host 404 è semplicemente una configurazione host che mostra una pagina 404.
Questo può essere utile quando il tuo dominio è elencato nei motori di ricerca e desideri fornire una pagina di errore più gradevole o specificare agli
indicizzatori di ricerca che le pagine del dominio non esistono più.
Un altro vantaggio di avere questo host è quello di tracciare i log degli accessi e
visualizzare i referrer.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/ProxyHosts.md
================================================
## Che cos'è un Proxy Host?
Un host proxy è l'endpoint in entrata per un servizio web che si desidera inoltrare.
Fornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato.
Gli host proxy sono l'uso più comune per Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/RedirectionHosts.md
================================================
## Che cos'è un Host di reindirizzamento?
Un Host di reindirizzamento reindirizza le richieste provenienti dal dominio in entrata e indirizza il
visitatore verso un altro dominio.
Il motivo più comune per utilizzare questo tipo di host è quando il tuo sito web cambia
dominio, ma hai ancora link di motori di ricerca o referrer che puntano al vecchio dominio.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/Streams.md
================================================
## Che cos'è uno Stream?
Una funzionalità relativamente nuova per Nginx, uno Stream serve a inoltrare il traffico TCP/UDP
direttamente a un altro computer sulla rete.
Se gestisci server di gioco, FTP o SSH, questa funzionalità può rivelarsi molto utile.
================================================
FILE: frontend/src/locale/src/HelpDoc/it/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/AccessLists.md
================================================
## アクセスリストとは
アクセスリストは特定のクライアントIPへのブラックリストとホワイトリストを提供し、ベーシック認証によるプロキシホストへの認証を可能にします。
複数のクライアントルールやユーザー名とパスワードを一つのアクセスリストに設定し、一つ以上の _プロキシホスト_ に適応することができます。
これは認証システムを持たないサービスや不明なクライアントからの保護が必要な場合に有効です。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/Certificates.md
================================================
## 証明書
### HTTP 証明書
HTTPによって検証された証明書はLet's EncryptサーバーがHTTPでドメインにアクセスを試みサーバーを管理していることを確認できた場合に発行される証明書です。
この方法では、HTTPアクセス可能でこのNginxを指しているドメインに対して _プロキシホスト_ を作成する必要があります。証明書が発行された後は、 _プロキシホスト_ を編集してこの証明書をHTTPS接続に使用するように設定できます。ただし、証明書の更新には、_プロキシホスト_ がHTTP接続用に設定された状態を維持する必要があります。
この方法はワイルドカードのドメインをサポート _していません_ 。
### DNS 証明書
DNSによって検証された証明書にはDNSプロバイダープラグインが必要です。このプロバイダーはドメイン上に一時レコードを作成するために使用されます。その後Let's Encryptサーバーがそのレコードを参照し、あなたが所有していることを確認できると証明書が発行されます。
このタイプの証明書を作成する際に、 _プロキシホスト_ を作成する必要はありません。また、_プロキシホスト_ をHTTPアクセス用に設定する必要もありません。
この方法はワイルドカードのドメインをサポート _します_ 。
### カスタム証明書
このオプションでは、あなたの証明書認証局によって提供された自身の証明書をアップロードして使用できます。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/DeadHosts.md
================================================
## 404ホストとはなんですか?
404ホストとは、単に404ページを表示するよう設定されたホストです。
これは、検索エンジンに登録されたドメインに分かりやすいエラーページを提供したい場合や、検索エンジンのインデクサーにドメインページがもう存在しないことを伝えたい場合に便利です。
このホストを持つもう一つの利点は、アクセスログを追跡し、参照元を確認できることです。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/ProxyHosts.md
================================================
## プロキシホストとは何ですか?
プロキシホストは転送したいwebサービスの受信エンドポイントです。
サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。
プロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/RedirectionHosts.md
================================================
## リダイレクトホストとは何ですか?
リダイレクトホストは受信したリクエストを別のドメインにリダイレクトして訪問者に表示します。
このタイプのもっとも一般的な使用理由は、webサイトのドメインが変更されたが検索エンジンやリンクが古いドメインを指し続けている場合です。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/Streams.md
================================================
## ストリームとは何ですか?
Nginxの比較的新しい機能であるストリームは、TCP/UDPトラフィックをネットワーク上の別のコンピュータに直接転送します。
ゲームサーバー、FTPサーバー、SSHサーバーを運用している場合に便利です。
================================================
FILE: frontend/src/locale/src/HelpDoc/ja/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/AccessLists.md
================================================
## 접근 정책이란?
접근 정책은 특정 클라이언트 IP 주소를 허용하거나 거부할 수 있으며,
프록시 호스트에 기본 HTTP 인증(Basic Auth) 을 적용할 수 있는 기능입니다.
하나의 접근 목록에 여러 클라이언트 규칙과 사용자 이름, 비밀번호를 추가한 뒤
이를 하나 이상의 프록시 호스트에 적용할 수 있습니다.
이 기능은 인증 기능이 없는 웹 서비스에 인증을 추가하거나,
알 수 없는 클라이언트로부터 서비스를 보호할 때 유용합니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/Certificates.md
================================================
## 인증서 도움말
### HTTP 인증서
HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**
이 방식은 **와일드카드 도메인을 지원하지 않습니다.**
---
### DNS 인증서
DNS 검증 방식의 인증서는 DNS 공급자 플러그인을 사용해야 합니다. 이 플러그인은 도메인에 임시 DNS 레코드를 생성하며, Let's Encrypt는 해당 레코드를 조회해 도메인 소유 여부를 확인합니다. 검증이 성공하면 인증서가 발급됩니다.
이 방식은 인증서를 요청하기 전에 **프록시 호스트를 생성할 필요가 없으며**, 프록시 호스트에 HTTP 접근을 설정할 필요도 없습니다.
이 방식은 **와일드카드 도메인을 지원합니다.**
---
### 사용자 지정 인증서
이 옵션을 사용하면 직접 보유한 인증 기관(CA)에서 발급한 SSL 인증서를 직접 업로드하여 사용할 수 있습니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/DeadHosts.md
================================================
## 404 호스트란?
404 호스트는 404 오류 페이지를 표시하도록 구성된 호스트입니다.
이 기능은 도메인이 검색 엔진에 이미 색인되어 있을 때,
더 깔끔한 오류 페이지를 제공하거나 해당 페이지가 더 이상 존재하지 않음을
검색 엔진에게 명확하게 알려야 할 때 유용합니다.
또한 404 호스트를 사용하면 접근 로그를 확인하고, 어떤 경로(Referrer)를 통해 들어왔는지 추적할 수 있다는 장점도 있습니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md
================================================
## 프록시 호스트란?
프록시 호스트는 외부에서 들어오는 웹 요청을 받아 지정한 전달 대상으로 전달하는 역할을 합니다.
원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.
프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md
================================================
## 리다이렉션 호스트란?
리다이렉션 호스트는 외부에서 들어오는 도메인 요청을 다른 도메인으로 자동 이동(리다이렉트)시키는 역할을 합니다.
이 유형의 호스트는 주로 웹사이트의 도메인이 변경되었지만,
검색 엔진이나 다른 사이트에 이전 도메인 링크가 남아 있을 때 사용하면 가장 효과적입니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/Streams.md
================================================
## 호스트 스트림이란?
호스트 스트림은 비교적 최근에 Nginx에 추가된 기능으로,
TCP/UDP 트래픽을 네트워크 내의 다른 컴퓨터로 직접 전달하는 데 사용됩니다.
게임 서버나 FTP, SSH 서버 등을 운영할 때 유용하게 사용할 수 있습니다.
================================================
FILE: frontend/src/locale/src/HelpDoc/ko/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/AccessLists.md
================================================
## Wat is een Toegangslijst?
Toeganslijsten bieden een zwarte- of witte lijst van specifieke client IP-adressen samen met authenticatie voor de Proxy Hosts via Basic HTTP Authenticatie.
Je kan meerdere client regels, gebruikersnamen en wachtwoorden voor een enkele Toegangslijst configureren en toepassen op één of meerdere _Proxy Hosts_.
Dit is het meest nuttig voor doorgestuurde webdiensten die geen authenticatiemechanismen hebben of wanneer je wilt beveiligen tegen onbekende bezoekers.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/Certificates.md
================================================
## Certificaten Hulp
### HTTP Certificaat
Een HTTP gevalideerd certificaat betekent dat Let's Encrypt servers
zullen proberen om over HTTP te bereiken (niet HTTPS!) en als dat gelukt is, zal
jouw certificaat worden uitgegeven.
Voor deze zal je een _Proxy Host_ moeten hebben die is toegankelijk via HTTP en
die naar deze Nginx installatie wijst. Nadat een certificaat is uitgegeven kan je
de _Proxy Host_ wijzigen om ook HTTPS toegang te geven. Maar de _Proxy Host_ zal
nog moeten worden geconfigureerd voor HTTP toegang om het certificaat te verlengen.
Dit proces ondersteunt geen domeinen met wildcards.
### DNS Certificaat
Een DNS gevalideerd certificaat zal gebruik maken van een DNS Provider plugin. De
DNS Provider zal tijdelijke records op jouw domein maken en Let's Encrypt zal deze
records opvragen om te controleren of je de eigenaar bent. Als dat is gecontroleerd
is zal Let's Encrypt het certificaat uitgeven.
Je hebt geen _Proxy Host_ nodig om dit soort certificaat aan te vragen. Je hebt dus
geen HTTP _Proxy Host_ nodig.
Dit proces ondersteunt _wel_ domeinen met wildcards.
### Aangepast Certificaat
Gebruik deze optie om jouw eigen SSL Certificaat te uploaden, zoals
geleverd door jouw eigen Certificate Authority.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/DeadHosts.md
================================================
## Wat is een 404 Host?
Simpel gezegd is een 404 Host een host setup die een 404 pagina weergeeft.
Dit kan nuttig zijn wanneer jouw domein is opgegeven in zoekmachines en je wil
een betere foutpagina leveren of specifiek om te zeggen tegen de zoekmachines dat
het domein niet langer bestaat.
Een ander voordeel van het hebben van een 404 Host is om de logs voor bezoeken
te volgen en de referenties te bekijken.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/ProxyHosts.md
================================================
## Wat is een Proxy Host?
Een Proxy Host is de inkomende endpoint voor een webdienst dat je wilt doorsturen.
Het biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft.
Proxy Hosts worden het meest gebruikt in Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/RedirectionHosts.md
================================================
## Wat is een Redirection Host?
Een Redirection Host zal verzoeken van de inkomende domeinnaam doorsturen, en de bezoeker
omleiden naar een andere domeinnaam.
Het gebruik van een Redirection Host is vooral handig wanneer je jouw website verandert
maar je nog zoekmachines of referenties naar de oude domeinnaam hebben.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/Streams.md
================================================
## Wat is een Stream?
Streams zijn een nieuwe toevoeging aan Nginx, die toelaat om TCP/UDP
verkeer naar een ander computer op het netwerk te sturen.
Als je game servers, FTP of SSH servers draait kan dit handig zijn.
================================================
FILE: frontend/src/locale/src/HelpDoc/nl/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/no/AccessLists.md
================================================
## Hva er en tilgangsliste?
Tilgangslister gir en svarteliste eller hviteliste over spesifikke klient‑IP‑adresser, sammen med autentisering for `Proxy‑hosts` via Basic HTTP‑autentisering.
Du kan konfigurere flere klientregler, brukernavn og passord for én tilgangsliste og deretter bruke denne på én eller flere `Proxy‑hosts`.
Dette er spesielt nyttig for videresendte webtjenester som ikke har innebygd autentisering, eller når du ønsker å beskytte mot ukjente klienter.
================================================
FILE: frontend/src/locale/src/HelpDoc/no/Certificates.md
================================================
## Hjelp om sertifikater
### HTTP‑sertifikat
Et HTTP‑validert sertifikat betyr at Let's Encrypt‑serverne vil forsøke å nå
domenene dine over HTTP (ikke HTTPS!) og hvis det lykkes, vil de utstede sertifikatet.
For denne metoden må du ha en `Proxy‑host` opprettet for domenet/domenene dine som
er tilgjengelig over HTTP og peker til denne Nginx‑installasjonen. Etter at et sertifikat
er utstedt, kan du endre `Proxy‑host` til også å bruke dette sertifikatet for HTTPS‑tilkoblinger.
Proxy‑hosten må imidlertid fortsatt være konfigurert for HTTP‑tilgang for at sertifikatet skal kunne fornyes.
Denne prosessen _støtter ikke_ wildcard‑domener.
### DNS‑sertifikat
Et DNS‑validert sertifikat krever at du bruker en DNS‑leverandør‑plugin. Denne leverandøren
vil opprette midlertidige DNS‑poster på domenet ditt, og Let's Encrypt vil deretter spørre
disse postene for å bekrefte at du eier domenet. Hvis valideringen lykkes, utstedes sertifikatet.
Du trenger ikke å ha en `Proxy‑host` opprettet før du ber om denne typen sertifikat. Du trenger heller
ikke at `Proxy‑host` er konfigurert for HTTP‑tilgang.
Denne prosessen _støtter_ wildcard‑domener.
### Egendefinert sertifikat
Bruk dette alternativet for å laste opp ditt eget SSL‑sertifikat, levert av din
egen sertifikatmyndighet (CA).
================================================
FILE: frontend/src/locale/src/HelpDoc/no/DeadHosts.md
================================================
## Hva er en 404‑host?
En 404‑host er enkelt og greit en host‑oppsett som viser en 404‑side.
Dette kan være nyttig når domenet ditt er oppført i søkemotorer og du ønsker å
vise en penere feilmelding, eller for å fortelle søkeindekser at sidene på domenet
ikke lenger eksisterer.
En annen fordel med å ha denne hosten er å kunne spore treff i loggene og
se hvilke henvisere som kommer til den.
================================================
FILE: frontend/src/locale/src/HelpDoc/no/ProxyHosts.md
================================================
## Hva er en Proxy‑host?
En Proxy‑host er inngangspunktet (innkommende endepunkt) for en webtjeneste du ønsker å videresende.
Den tilbyr valgfri SSL‑terminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL.
Proxy‑hosts er den vanligste bruken av Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/no/RedirectionHosts.md
================================================
## Hva er en omdirigerings‑host?
En omdirigerings‑host omdirigerer forespørsler fra det innkommende domenet og videresender
brukeren til et annet domene.
Den vanligste årsaken til å bruke denne typen host er når nettstedet ditt har byttet
domene, men søkemotorer eller henvisningslenker fortsatt peker til det gamle domenet.
================================================
FILE: frontend/src/locale/src/HelpDoc/no/Streams.md
================================================
## Hva er en Stream?
En relativt ny funksjon i Nginx. En Stream brukes til å videresende TCP/UDP‑trafikk
direkte til en annen maskin i nettverket.
Dette er nyttig hvis du kjører spillservere, FTP‑ eller SSH‑servere.
================================================
FILE: frontend/src/locale/src/HelpDoc/no/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/AccessLists.md
================================================
## Czym jest lista dostępu?
Listy 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.
Moż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.
Jest 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/Certificates.md
================================================
## Pomoc dotycząca certyfikatów
### Certyfikat HTTP
Certyfikat 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.
W przypadku tej metody musisz mieć utworzony Host proxy dla swoich domen, który jest dostępny przez HTTP i wskazuje na tę instalację Nginx.
Po 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.
Ten proces nie obsługuje domen wieloznacznych (wildcard).
### Certyfikat DNS
Certyfikat 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.
Nie 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.
Ten proces obsługuje domeny wieloznaczne (wildcard).
### Własny certyfikat
Użyj tej opcji, aby przesłać własny certyfikat SSL, dostarczony przez twój własny urząd certyfikacji.
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/DeadHosts.md
================================================
## Czym jest host 404?
Host 404 to po prostu konfiguracja hosta, która wyświetla stronę 404.
Moż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ą.
Kolejną zaletą posiadania tego hosta jest możliwość śledzenia logów dla odwiedzin oraz przeglądania źródeł ruchu (referrerów).
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/ProxyHosts.md
================================================
## Czym jest host proxy?
Host proxy to punkt wejściowy dla usługi internetowej, którą chcesz przekierować.
Zapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL.
Hosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/RedirectionHosts.md
================================================
## Czym jest host przekierowania?
Host przekierowania przekierowuje żądania z domeny przychodzącej i przenosi odwiedzającego na inną domenę.
Najczę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ę.
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/Streams.md
================================================
## Czym jest strumień?
Stosunkowo nowa funkcja dla Nginx, strumień służy do przekazywania ruchu TCP/UDP bezpośrednio na inny komputer/serwer w sieci.
Jeśli prowadzisz serwery gier, FTP lub SSH, może się to okazać przydatne
================================================
FILE: frontend/src/locale/src/HelpDoc/pl/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/AccessLists.md
================================================
## O que é uma Access List?
As *Access Lists* fornecem uma lista de permissões (whitelist) ou bloqueios (blacklist)
de endereços IP específicos de clientes, juntamente com autenticação para os *Proxy Hosts*
via Autenticação HTTP Básica (*Basic Auth*).
Podes configurar múltiplas regras de cliente, nomes de utilizador e palavras-passe
para uma única *Access List*, e depois aplicá-la a um ou mais *Proxy Hosts*.
Isto é especialmente útil para serviços web encaminhados que não têm mecanismos
de autenticação integrados ou quando pretendes proteger o acesso contra clientes desconhecidos.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/Certificates.md
================================================
## Ajuda de Certificados
### Certificado HTTP
Um certificado validado por HTTP significa que os servidores do Let's Encrypt irão
tentar aceder aos teus domínios via HTTP (não HTTPS!) e, se a ligação for bem-sucedida,
emitirão o certificado.
Para este método, é necessário ter um *Proxy Host* criado para o(s) teu(s) domínio(s),
acessível via HTTP e a apontar para esta instalação do Nginx. Depois de o certificado ser
emitido, podes modificar o *Proxy Host* para também utilizar esse certificado em ligações HTTPS.
No entanto, o *Proxy Host* deve continuar configurado para acesso HTTP para que a renovação
funcione corretamente.
Este processo **não** suporta domínios wildcard.
### Certificado DNS
Um certificado validado por DNS requer que uses um plugin de fornecedor DNS (*DNS Provider*).
Este fornecedor será usado para criar registos temporários no teu domínio, que serão consultados
pelo Let's Encrypt para confirmar que és o proprietário. Se tudo correr bem, o certificado será emitido.
Não é necessário ter um *Proxy Host* criado antes de pedir este tipo de certificado.
Também não é necessário que o *Proxy Host* tenha acesso HTTP configurado.
Este processo **suporta** domínios wildcard.
### Certificado Personalizado
Usa esta opção para carregar o teu próprio Certificado SSL, fornecido pela
tua Autoridade Certificadora.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/DeadHosts.md
================================================
## O que é um 404 Host?
Um *404 Host* é simplesmente um host configurado para apresentar uma página 404.
Isto pode ser útil quando o teu domínio aparece em motores de busca e queres fornecer
uma página de erro mais agradável ou indicar especificamente aos indexadores de pesquisa
que as páginas desse domínio já não existem.
Outra vantagem é permitir consultar os registos de acessos a este host e ver os referenciadores.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/ProxyHosts.md
================================================
## O que é um Proxy Host?
Um *Proxy Host* é o ponto de entrada para um serviço web que pretendes encaminhar.
Permite, opcionalmente, fazer terminação SSL para um serviço que possa não ter suporte SSL nativo.
Os *Proxy Hosts* são a utilização mais comum do Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/RedirectionHosts.md
================================================
## O que é um Redirection Host?
Um *Redirection Host* redireciona pedidos recebidos no domínio de entrada e envia
o utilizador para outro domínio.
A razão mais comum para usar este tipo de host é quando o teu site muda de domínio
mas ainda tens motores de busca ou links de referência a apontar para o domínio antigo.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/Streams.md
================================================
## O que é um Stream?
Uma funcionalidade relativamente recente no Nginx, um *Stream* serve para encaminhar
tráfego TCP/UDP diretamente para outro computador na rede.
Se estiveres a executar servidores de jogos, FTP ou SSH, isto pode ser bastante útil.
================================================
FILE: frontend/src/locale/src/HelpDoc/pt/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/AccessLists.md
================================================
## Что такое список доступа?
Списки доступа позволяют задавать белый/чёрный список IP‑адресов клиентов и настраивать аутентификацию для прокси‑хостов через базовую HTTP‑аутентификацию.
Для одного списка доступа можно настроить несколько правил клиентов, логины и пароли, а затем применить его к одному или нескольким _прокси‑хостам_.
Это особенно полезно для проксируемых веб‑сервисов без встроенной аутентификации или когда нужно защититься от неизвестных клиентов.
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/Certificates.md
================================================
## Справка по сертификатам
### HTTP-сертификат
Сертификат, подтверждённый по HTTP, означает, что серверы Let's Encrypt попытаются обратиться к вашим доменам по HTTP (не HTTPS!) и при успехе выпустят сертификат.
Для этого метода должен существовать _прокси‑хост_ для ваших доменов, доступный по HTTP и указывающий на эту установку Nginx. После выдачи сертификата вы можете настроить _прокси‑хост_ на использование этого сертификата для HTTPS‑подключений. Однако доступ по HTTP должен сохраняться, чтобы сертификат мог обновляться.
Этот способ _не_ поддерживает wildcard‑домены.
### DNS-сертификат
Сертификат, подтверждённый по DNS, требует использования плагина DNS‑провайдера. Такой провайдер создаст временные записи в вашем домене, затем Let's Encrypt проверит эти записи, чтобы убедиться, что вы владелец домена, и при успехе выпустит сертификат.
Для запроса такого сертификата предварительно создавать _прокси‑хост_ не требуется. Также не нужен доступ по HTTP для вашего _прокси‑хоста_.
Этот способ _поддерживает_ wildcard‑домены.
### Свой сертификат
Используйте этот вариант, чтобы загрузить собственный SSL‑сертификат, выданный вашим удостоверяющим центром (CA).
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/DeadHosts.md
================================================
## Что такое 404‑хост?
404‑хост — это конфигурация, которая показывает страницу 404.
Это полезно, когда ваш домен присутствует в поисковых системах и вы хотите показать более дружелюбную страницу ошибки или явно сообщить индексаторам, что страницы домена больше не существуют.
Ещё одно преимущество — можно отдельно отслеживать обращения в журналах и смотреть источники переходов.
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/ProxyHosts.md
================================================
## Что такое прокси‑хост?
Прокси‑хост — это входная точка веб‑сервиса, который вы проксируете.
Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL.
Прокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/RedirectionHosts.md
================================================
## Что такое редирект‑хост?
Редирект‑хост перенаправляет запросы, поступающие на входящий домен, на другой домен.
Чаще всего это используют, когда сайт сменил домен, а в поиске или на сторонних ресурсах всё ещё остаются ссылки на старый домен.
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/Streams.md
================================================
## Что такое поток?
Относительно новая возможность Nginx: поток позволяет напрямую проксировать TCP/UDP‑трафик на другой компьютер в сети.
Полезно для игровых серверов, FTP или SSH‑серверов.
================================================
FILE: frontend/src/locale/src/HelpDoc/ru/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/AccessLists.md
================================================
## Čo je zoznam prístupov?
Zoznamy 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.
Môž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.
Toto 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.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/Certificates.md
================================================
## Pomoc s certifikátmi
### Certifikát HTTP
Certifikát overený protokolom HTTP znamená, že servery Let's Encrypt sa
pokúsia pripojiť k vašim doménam cez protokol HTTP (nie HTTPS!) a v prípade úspechu
vydajú váš certifikát.
Pre túto metódu budete musieť mať pre svoje domény vytvorený _Proxy Host_, ktorý
je prístupný cez HTTP a smeruje na túto inštaláciu Nginx. Po vydaní certifikátu
môžete zmeniť _Proxy Host_ tak, aby tento certifikát používal aj pre HTTPS
pripojenia. _Proxy Host_ však bude stále potrebné nakonfigurovať pre prístup cez HTTP,
aby sa certifikát mohol obnoviť.
Tento proces _nepodporuje_ domény s divokými kartami.
### Certifikát DNS
Certifikát overený DNS vyžaduje použitie pluginu DNS Provider. Tento DNS
Provider sa použije na vytvorenie dočasných záznamov vo vašej doméne a potom Let's
Encrypt overí tieto záznamy, aby sa uistil, že ste vlastníkom, a ak bude úspešný,
vydá váš certifikát.
Pred požiadaním o tento typ certifikátu nie je potrebné vytvoriť _Proxy Host_.
Tiež nie je potrebné mať _Proxy Host_ nakonfigurovaný pre prístup HTTP.
Tento proces _podporuje_ domény s divokými kartami.
### Vlastný certifikát
Túto možnosť použite na nahratie vlastného SSL certifikátu, ktorý vám poskytla vaša
certifikačná autorita.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/DeadHosts.md
================================================
## Čo je to 404 Hostiteľ?
404 Hostiteľ je jednoducho nastavenie hostiteľa, ktoré zobrazuje stránku 404.
To môže byť užitočné, ak je vaša doména uvedená vo vyhľadávačoch a chcete
poskytnúť krajšiu stránku s chybou alebo konkrétne oznámiť vyhľadávačom, že
stránky domény už neexistujú.
Ďalšou výhodou tohto hostiteľa je sledovanie protokolov o návštevách a
zobrazenie odkazov.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/ProxyHosts.md
================================================
## Čo je proxy hostiteľ?
Proxy hostiteľ je prichádzajúci koncový bod pre webovú službu, ktorú chcete presmerovať.
Poskytuje voliteľné ukončenie SSL pre vašu službu, ktorá nemusí mať zabudovanú podporu SSL.
Proxy hostitelia sú najbežnejším použitím pre Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/RedirectionHosts.md
================================================
## Čo je presmerovací hostiteľ?
Presmerovací hostiteľ presmeruje požiadavky z prichádzajúcej domény a presmeruje
návštevníka na inú doménu.
Najčastejším dôvodom na použitie tohto typu hostiteľa je situácia, keď vaša webová stránka zmení
doménu, ale stále máte odkazy vo vyhľadávačoch alebo referenčné odkazy smerujúce na starú doménu.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/Streams.md
================================================
## Čo je stream?
Stream je relatívne nová funkcia pre Nginx, ktorá slúži na presmerovanie TCP/UDP
dátového toku priamo do iného počítača v sieti.
Ak prevádzkujete herné servery, FTP alebo SSH servery, táto funkcia sa vám môže hodiť.
================================================
FILE: frontend/src/locale/src/HelpDoc/sk/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/AccessLists.md
================================================
## Erişim Listesi Nedir?
Eriş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.
Tek 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.
Bu, yerleşik kimlik doğrulama mekanizmaları olmayan veya bilinmeyen istemcilerden korunmak istediğinizde iletilen web hizmetleri için en kullanışlıdır.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/Certificates.md
================================================
## Sertifika Yardımı
### HTTP Sertifikası
Bir HTTP doğrulanmış sertifika, Let's Encrypt sunucularının
alan adlarınıza HTTP (HTTPS değil!) üzerinden ulaşmaya çalışacağı ve başarılı olursa,
sertifikanızı verecekleri anlamına gelir.
Bu 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
verildikten sonra, _Proxy Host_'u HTTPS
bağ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.
Bu işlem joker karakter alan adlarını _desteklemez_.
### DNS Sertifikası
Bir DNS doğrulanmış sertifika, bir DNS Sağlayıcı eklentisi kullanmanızı gerektirir. Bu DNS
Sağlayıcı, alan adınızda geçici kayıtlar oluşturmak için kullanılacak ve ardından Let's
Encrypt bu kayıtları sorgulayarak sahibi olduğunuzdan emin olacak ve başarılı olursa,
sertifikanızı verecektir.
Bu 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.
Bu işlem joker karakter alan adlarını _destekler_.
### Özel Sertifika
Kendi Sertifika Otoriteniz tarafından sağlanan kendi SSL Sertifikanızı yüklemek için bu seçeneği kullanın.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/DeadHosts.md
================================================
## 404 Host Nedir?
404 Host, basitçe bir 404 sayfası gösteren bir host kurulumudur.
Bu, alan adınız arama motorlarında listelendiğinde ve daha güzel bir hata sayfası sağlamak veya özellikle arama dizinleyicilerine
alan adı sayfalarının artık mevcut olmadığını söylemek istediğinizde yararlı olabilir.
Bu host'un bir başka faydası da, ona yapılan isteklerin loglarını takip etmek ve
referansları görüntülemektir.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/ProxyHosts.md
================================================
## Proxy Host Nedir?
Proxy Host, iletilmek istediğiniz bir web hizmeti için gelen uç noktadır.
SSL desteği yerleşik olmayan hizmetiniz için isteğe bağlı SSL sonlandırma sağlar.
Proxy Host'lar, Nginx Proxy Manager'ın en yaygın kullanımıdır.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/RedirectionHosts.md
================================================
## Yönlendirme Host'u Nedir?
Yönlendirme Host'u, gelen alan adından gelen istekleri yönlendirir ve
görüntüleyiciyi başka bir alan adına yönlendirir.
Bu tür bir host kullanmanın en yaygın nedeni, web sitenizin alan adı değiştiğinde
ancak hala eski alan adına işaret eden arama motoru veya referans bağlantılarınız olduğunda ortaya çıkar.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/Streams.md
================================================
## Akış Nedir?
Nginx için nispeten yeni bir özellik olan Akış, TCP/UDP
trafiğini doğrudan ağdaki başka bir bilgisayara iletmek için hizmet edecektir.
Oyun sunucuları, FTP veya SSH sunucuları çalıştırıyorsanız bu işinize yarayabilir.
================================================
FILE: frontend/src/locale/src/HelpDoc/tr/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/AccessLists.md
================================================
## Khái niệm Access List là gì?
Access 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.
Bạ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.
Tính năng này đặc biệt hữu ích đối với:
các dịch vụ web được forward mà không có cơ chế xác thực tích hợp, hoặc
khi bạn muốn bảo vệ tài nguyên khỏi những client không xác định.
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/Certificates.md
================================================
## Hỗ trợ Chứng chỉ
### Chứng chỉ HTTP (HTTP Certificate)
Chứ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.
Vớ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.
Sau 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.
Tuy 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.
Phương thức này _không hỗ trợ_ wildcard domain.
### Chứng chỉ DNS (DNS Certificate)
Chứng chỉ được xác thực qua DNS yêu cầu bạn sử dụng plugin của DNS Provider.
Plugin 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.
Khi 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.
Phương thức DNS _có hỗ trợ_ wildcard domain.
### Chứng chỉ tùy chỉnh (Custom Certificate)
Tù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.
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/DeadHosts.md
================================================
## 404 Host là gì?
404 Host đơn giản là một host được thiết lập để hiển thị trang 404.
Đ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.
Mộ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à
xem các nguồn giới thiệu (referrers).
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md
================================================
## Proxy Host là gì?
Proxy 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ó 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.
Proxy Host là loại cấu hình phổ biến nhất trong Nginx Proxy Manager.
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md
================================================
## Redirection Host là gì?
Redirection 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
Lý 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ũ.
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/Streams.md
================================================
## Stream là gì?
Stream 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
TCP/UDP trực tiếp tới một máy khác trong mạng.
Nế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.
================================================
FILE: frontend/src/locale/src/HelpDoc/vi/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/AccessLists.md
================================================
## 什么是通信规则?
通信规则提供了一个特定客户IP地址的黑名单或白名单,以及通过基本HTTP认证对代理服务的认证。
你可以为一个通信规则配置多个客户规则、用户名和密码,然后将其应用于代理服务。
这对那些没有内置认证机制的转发网络服务或你想保护其免受未知客户的访问是最有用的。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/Certificates.md
================================================
## 证书帮助
### HTTP 证书
HTTP 验证的证书表示 Let's Encrypt 服务器将尝试通过 HTTP(而非 HTTPS!)访问您的域名,如果成功,它们将为您颁发证书。
使用此方法时,您必须为您的域名创建一个可通过 HTTP 访问并指向此 Nginx 安装的 代理主机。在获得证书后,您可以修改该 代理主机,使其也使用此证书处理 HTTPS 连接。然而,为了证书能够续期,该 代理主机 仍需配置为支持 HTTP 访问。
此过程_不支持_通配符域名。
### DNS 证书
DNS 验证的证书要求您使用一个 DNS 服务商插件。该 DNS 服务商将用于在您的域名下创建临时记录,随后 Let's Encrypt 将查询这些记录以确认您是域名所有者,如果成功,它们将为您颁发证书。
请求此类证书前,您无需预先创建 代理主机,也无需将您的 代理主机 配置为支持 HTTP 访问。
此过程_支持_通配符域名。
### 自定义证书
使用此选项上传您自己的 SSL 证书,该证书由您自己的证书颁发机构提供。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/DeadHosts.md
================================================
## 什么是错误页面?
错误页面是一个简单的主机设置,显示错误页面。
当你的域名被列入搜索引擎,而你想提供一个更好的错误页面或特别是告诉搜索索引者域名页面不再存在时,这可能是有用的。
拥有这种主机的另一个好处是可以跟踪点击它的日志并查看访问来源。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/ProxyHosts.md
================================================
## 什么是代理服务?
代理服务是你想转发网络应用的主机。
代理服务可以为没有SSL服务的网络应用提供SSL服务(可选)。
代理服务是Nginx代理管理器的最常见用途之一。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/RedirectionHosts.md
================================================
## 什么是重定向?
重定向是将接入域名的请求推送到另一个域名。
使用这种类型的主机最常见的原因是当你的网站改变了域名,但你仍然有链接指向旧域名的应用。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/Streams.md
================================================
## 什么是端口转发?
端口转发是Nginx的一个相对较新的功能,可以直接转发 TCP/UDP 流量到网络上的另一台计算机。
如果你正在运行游戏服务器、FTP或SSH服务器,这个功能就会很有用。
================================================
FILE: frontend/src/locale/src/HelpDoc/zh/index.ts
================================================
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";
================================================
FILE: frontend/src/locale/src/bg.json
================================================
{
"access-list": {
"defaultMessage": "Списък за достъп"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {правило} other {правила}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {потребител} other {потребители}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Когато съществува поне 1 правило, това правило за отказ се добавя последно"
},
"access-list.help.rules-order": {
"defaultMessage": "Обърнете внимание, че правилата Позволяване и Отказване се прилагат в реда, в който са зададени."
},
"access-list.pass-auth": {
"defaultMessage": "Предаване на автентикация към Upstream"
},
"access-list.public": {
"defaultMessage": "Публичен достъп"
},
"access-list.public.subtitle": {
"defaultMessage": "Без базова автентикация"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 или 192.168.1.0/24 или 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Удовлетворяване на което и да е"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {потребител} other {потребители}}, {rules} {rules, plural, one {правило} other {правила}} - Създадено: {date}"
},
"access-lists": {
"defaultMessage": "Списъци за достъп"
},
"action.add": {
"defaultMessage": "Добавяне"
},
"action.add-location": {
"defaultMessage": "Добавяне на маршрут"
},
"action.allow": {
"defaultMessage": "Разрешаване"
},
"action.close": {
"defaultMessage": "Затваряне"
},
"action.delete": {
"defaultMessage": "Изтриване"
},
"action.deny": {
"defaultMessage": "Отказване"
},
"action.disable": {
"defaultMessage": "Деактивиране"
},
"action.download": {
"defaultMessage": "Изтегляне"
},
"action.edit": {
"defaultMessage": "Редактиране"
},
"action.enable": {
"defaultMessage": "Активиране"
},
"action.permissions": {
"defaultMessage": "Права"
},
"action.renew": {
"defaultMessage": "Подновяване"
},
"action.view-details": {
"defaultMessage": "Преглед на детайли"
},
"auditlogs": {
"defaultMessage": "Журнали за одит"
},
"auto": {
"defaultMessage": "Автоматично"
},
"cancel": {
"defaultMessage": "Отказ"
},
"certificate": {
"defaultMessage": "Сертификат"
},
"certificate.custom-certificate": {
"defaultMessage": "Сертификат"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Ключ на сертификата"
},
"certificate.custom-intermediate": {
"defaultMessage": "Междинен сертификат"
},
"certificate.in-use": {
"defaultMessage": "Използва се"
},
"certificate.none.subtitle": {
"defaultMessage": "Не е назначен сертификат"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Този хост няма да използва HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Без сертификат"
},
"certificate.not-in-use": {
"defaultMessage": "Не се използва"
},
"certificate.renew": {
"defaultMessage": "Подновяване на сертификат"
},
"certificates": {
"defaultMessage": "Сертификати"
},
"certificates.custom": {
"defaultMessage": "Потребителски сертификат"
},
"certificates.custom.warning": {
"defaultMessage": "Ключове, защитени с парола, не се поддържат."
},
"certificates.dns.credentials": {
"defaultMessage": "Съдържание на файл с удостоверения"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Този плъгин изисква конфигурационен файл с API токен или други идентификационни данни."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Тези данни ще бъдат съхранени като обикновен текст в базата и във файл!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Секунди за разпространение"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Оставете празно, за да се използва стойността по подразбиране. Брой секунди за изчакване на DNS разпространение."
},
"certificates.dns.provider": {
"defaultMessage": "DNS доставчик"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Изберете доставчик..."
},
"certificates.dns.warning": {
"defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията."
},
"certificates.http.reachability-404": {
"defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Няма достъпен сървър на този домейн. Проверете, че домейнът съществува и сочи към IP-та, където се изпълнява NPM, и ако е необходимо, че порт 80 е пренасочен."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Вашият сървър е достъпен и създаването на сертификати е възможно."
},
"certificates.http.reachability-other": {
"defaultMessage": "Намерен е сървър, но върна неочакван код {code}. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "Намерен е сървър, но върна неочаквани данни. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър."
},
"certificates.http.test-results": {
"defaultMessage": "Резултати от теста"
},
"certificates.http.warning": {
"defaultMessage": "Тези домейни трябва вече да сочат към тази инсталация."
},
"certificates.key-type": {
"defaultMessage": "Тип ключ"
},
"certificates.key-type-description": {
"defaultMessage": "RSA е широко съвместим, ECDSA е по-бърз и по-сигурен, но може да не се поддържа от по-стари системи"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "с Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Заявка за нов сертификат"
},
"column.access": {
"defaultMessage": "Достъп"
},
"column.authorization": {
"defaultMessage": "Автентикация"
},
"column.authorizations": {
"defaultMessage": "Автентикации"
},
"column.custom-locations": {
"defaultMessage": "Персонализирани маршрути"
},
"column.destination": {
"defaultMessage": "Дестинация"
},
"column.details": {
"defaultMessage": "Детайли"
},
"column.email": {
"defaultMessage": "Имейл"
},
"column.event": {
"defaultMessage": "Събитие"
},
"column.expires": {
"defaultMessage": "Изтича"
},
"column.http-code": {
"defaultMessage": "HTTP код"
},
"column.incoming-port": {
"defaultMessage": "Входящ порт"
},
"column.name": {
"defaultMessage": "Име"
},
"column.protocol": {
"defaultMessage": "Протокол"
},
"column.provider": {
"defaultMessage": "Доставчик"
},
"column.roles": {
"defaultMessage": "Роли"
},
"column.rules": {
"defaultMessage": "Правила"
},
"column.satisfy": {
"defaultMessage": "Удовлетворяване"
},
"column.satisfy-all": {
"defaultMessage": "Всички"
},
"column.satisfy-any": {
"defaultMessage": "Кое и да е"
},
"column.scheme": {
"defaultMessage": "Схема"
},
"column.source": {
"defaultMessage": "Източник"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Статус"
},
"created-on": {
"defaultMessage": "Създадено: {date}"
},
"dashboard": {
"defaultMessage": "Табло"
},
"dead-host": {
"defaultMessage": "404 хост"
},
"dead-hosts": {
"defaultMessage": "404 хостове"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 хост} other {404 хостове}}"
},
"disabled": {
"defaultMessage": "Деактивиран"
},
"domain-names": {
"defaultMessage": "Домейн имена"
},
"domain-names.max": {
"defaultMessage": "Максимум {count} домейна"
},
"domain-names.placeholder": {
"defaultMessage": "Започнете да въвеждате, за да добавите домейн..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcard не е разрешен за този тип"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcard не се поддържа от това CA"
},
"domains.force-ssl": {
"defaultMessage": "Принудително SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS активирано"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS за поддомейни"
},
"domains.http2-support": {
"defaultMessage": "Поддръжка на HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Използване на DNS Challenge"
},
"email-address": {
"defaultMessage": "Имейл адрес"
},
"empty-search": {
"defaultMessage": "Няма резултати"
},
"empty-subtitle": {
"defaultMessage": "Защо не създадете един?"
},
"enabled": {
"defaultMessage": "Активиран"
},
"error.access.at-least-one": {
"defaultMessage": "Необходимо е поне една Автентикация или едно Правило за достъп"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Потребителските имена за достъп трябва да са уникални"
},
"error.invalid-auth": {
"defaultMessage": "Невалиден имейл или парола"
},
"error.invalid-domain": {
"defaultMessage": "Невалиден домейн: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Невалиден имейл адрес"
},
"error.max-character-length": {
"defaultMessage": "Максималната дължина е {max} знак{max, plural, one {} other {а}}"
},
"error.max-domains": {
"defaultMessage": "Твърде много домейни, максимум {max}"
},
"error.maximum": {
"defaultMessage": "Максимум {max}"
},
"error.min-character-length": {
"defaultMessage": "Минималната дължина е {min} знак{min, plural, one {} other {а}}"
},
"error.minimum": {
"defaultMessage": "Минимум e {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Паролите трябва да съвпадат"
},
"error.required": {
"defaultMessage": "Това поле е задължително"
},
"expires.on": {
"defaultMessage": "Изтича: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork в GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Блокиране на често срещани експлойти"
},
"host.flags.cache-assets": {
"defaultMessage": "Кеширане на ресурси"
},
"host.flags.preserve-path": {
"defaultMessage": "Запазване на пътя"
},
"host.flags.protocols": {
"defaultMessage": "Протоколи"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Поддръжка на WebSockets"
},
"host.forward-port": {
"defaultMessage": "Порт"
},
"host.forward-scheme": {
"defaultMessage": "Схема"
},
"hosts": {
"defaultMessage": "Хостове"
},
"http-only": {
"defaultMessage": "Само HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt чрез DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt чрез HTTP"
},
"loading": {
"defaultMessage": "Зареждане…"
},
"login.title": {
"defaultMessage": "Вход в акаунта"
},
"nginx-config.label": {
"defaultMessage": "Персонализирана Nginx конфигурация"
},
"nginx-config.placeholder": {
"defaultMessage": "# Въведете вашата персонализирана Nginx конфигурация на собствен риск!"
},
"no-permission-error": {
"defaultMessage": "Нямате достъп до тази страница."
},
"notfound.action": {
"defaultMessage": "Към началната страница"
},
"notfound.content": {
"defaultMessage": "Страницата, която търсите, не беше намерена"
},
"notfound.title": {
"defaultMessage": "Упс… Намерихте грешка"
},
"notification.error": {
"defaultMessage": "Грешка"
},
"notification.object-deleted": {
"defaultMessage": "{object} беше изтрит"
},
"notification.object-disabled": {
"defaultMessage": "{object} беше деактивиран"
},
"notification.object-enabled": {
"defaultMessage": "{object} беше активиран"
},
"notification.object-renewed": {
"defaultMessage": "{object} беше подновен"
},
"notification.object-saved": {
"defaultMessage": "{object} беше запазен"
},
"notification.success": {
"defaultMessage": "Успех"
},
"object.actions-title": {
"defaultMessage": "{object} №{id}"
},
"object.add": {
"defaultMessage": "Добавяне: {object}"
},
"object.delete": {
"defaultMessage": "Изтриване: {object}"
},
"object.delete.content": {
"defaultMessage": "Сигурни ли сте, че искате да изтриете {object}?"
},
"object.edit": {
"defaultMessage": "Редактиране: {object}"
},
"object.empty": {
"defaultMessage": "Няма налични {objects}"
},
"object.event.created": {
"defaultMessage": "Създаден {object}"
},
"object.event.deleted": {
"defaultMessage": "Изтрит {object}"
},
"object.event.disabled": {
"defaultMessage": "Деактивиран {object}"
},
"object.event.enabled": {
"defaultMessage": "Активиран {object}"
},
"object.event.renewed": {
"defaultMessage": "Подновен {object}"
},
"object.event.updated": {
"defaultMessage": "Актуализиран {object}"
},
"offline": {
"defaultMessage": "Офлайн"
},
"online": {
"defaultMessage": "Онлайн"
},
"options": {
"defaultMessage": "Опции"
},
"password": {
"defaultMessage": "Парола"
},
"password.generate": {
"defaultMessage": "Генериране на случайна парола"
},
"password.hide": {
"defaultMessage": "Скриване на паролата"
},
"password.show": {
"defaultMessage": "Показване на паролата"
},
"permissions.hidden": {
"defaultMessage": "Скрито"
},
"permissions.manage": {
"defaultMessage": "Управление"
},
"permissions.view": {
"defaultMessage": "Само преглед"
},
"permissions.visibility.all": {
"defaultMessage": "Всички елементи"
},
"permissions.visibility.title": {
"defaultMessage": "Видимост на елементите"
},
"permissions.visibility.user": {
"defaultMessage": "Само създадените от потребителя"
},
"proxy-host": {
"defaultMessage": "Прокси хост"
},
"proxy-host.forward-host": {
"defaultMessage": "Хост/IP за препращане"
},
"proxy-hosts": {
"defaultMessage": "Прокси хостове"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {прокси хост} other {прокси хостове}}"
},
"public": {
"defaultMessage": "Публичен"
},
"redirection-host": {
"defaultMessage": "Хост за пренасочване"
},
"redirection-host.forward-domain": {
"defaultMessage": "Домейн за пренасочване"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP код"
},
"redirection-hosts": {
"defaultMessage": "Хостове за пренасочване"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {хост за пренасочване} other {хостове за пренасочване}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiple Choices"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Преместено постоянно"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Преместено временно"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 See other"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Временно пренасочване"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Постоянно пренасочване"
},
"role.admin": {
"defaultMessage": "Администратор"
},
"role.standard-user": {
"defaultMessage": "Обикновен потребител"
},
"save": {
"defaultMessage": "Запазване"
},
"setting": {
"defaultMessage": "Настройка"
},
"settings": {
"defaultMessage": "Настройки"
},
"settings.default-site": {
"defaultMessage": "Сайт по подразбиране"
},
"settings.default-site.404": {
"defaultMessage": "404 страница"
},
"settings.default-site.444": {
"defaultMessage": "Без отговор (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Страница поздравление"
},
"settings.default-site.description": {
"defaultMessage": "Какво да се показва при заявка към неизвестен хост"
},
"settings.default-site.html": {
"defaultMessage": "Персонализиран HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Пренасочване"
},
"setup.preamble": {
"defaultMessage": "Започнете, като създадете администраторски акаунт."
},
"setup.title": {
"defaultMessage": "Добре дошли!"
},
"sign-in": {
"defaultMessage": "Вход"
},
"ssl-certificate": {
"defaultMessage": "SSL сертификат"
},
"stream": {
"defaultMessage": "Поток"
},
"stream.forward-host": {
"defaultMessage": "Хост за препращане"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com или 10.0.0.1 или 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Входящ порт"
},
"streams": {
"defaultMessage": "Потоци"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {поток} other {потоци}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Тест"
},
"update-available": {
"defaultMessage": "Налична актуализация: {latestVersion}"
},
"user": {
"defaultMessage": "Потребител"
},
"user.change-password": {
"defaultMessage": "Смяна на парола"
},
"user.confirm-password": {
"defaultMessage": "Потвърждение на парола"
},
"user.current-password": {
"defaultMessage": "Текуща парола"
},
"user.edit-profile": {
"defaultMessage": "Редактиране на профил"
},
"user.full-name": {
"defaultMessage": "Пълно име"
},
"user.login-as": {
"defaultMessage": "Вход като {name}"
},
"user.logout": {
"defaultMessage": "Изход"
},
"user.new-password": {
"defaultMessage": "Нова парола"
},
"user.nickname": {
"defaultMessage": "Псевдоним"
},
"user.set-password": {
"defaultMessage": "Задаване на парола"
},
"user.set-permissions": {
"defaultMessage": "Настройка на права за {name}"
},
"user.switch-dark": {
"defaultMessage": "Тъмна тема"
},
"user.switch-light": {
"defaultMessage": "Светла тема"
},
"username": {
"defaultMessage": "Потребителско име"
},
"users": {
"defaultMessage": "Потребители"
}
}
================================================
FILE: frontend/src/locale/src/cs.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Počet zbývajících záložních kódů: {count}"
},
"2fa.backup-warning": {
"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."
},
"2fa.disable": {
"defaultMessage": "Vypnout dvoufaktorové ověřování"
},
"2fa.disable-confirm": {
"defaultMessage": "Vypnout 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Vypnutím dvoufaktorového ověřování snížíte bezpečnost svého účtu."
},
"2fa.disabled": {
"defaultMessage": "Vypnuto"
},
"2fa.done": {
"defaultMessage": "Uložil jsem si své záložní kódy."
},
"2fa.enable": {
"defaultMessage": "Zapnout dvoufaktorové ověřování"
},
"2fa.enabled": {
"defaultMessage": "Zapnuto"
},
"2fa.enter-code": {
"defaultMessage": "Zadejte ověřovací kód"
},
"2fa.enter-code-disable": {
"defaultMessage": "Zadejte ověřovací kód pro vypnutí"
},
"2fa.regenerate": {
"defaultMessage": "Znovu vytvořit"
},
"2fa.regenerate-backup": {
"defaultMessage": "Znovu vytvořit záložní kódy"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Zadejte ověřovací kód pro vytvoření nových záložních kódů. Vaše staré kódy budou neplatné."
},
"2fa.secret-key": {
"defaultMessage": "Tajný klíč"
},
"2fa.setup-instructions": {
"defaultMessage": "Naskenujte tento QR kód pomocí své ověřovací aplikace nebo zadejte tajný klíč ručně."
},
"2fa.status": {
"defaultMessage": "Stav"
},
"2fa.title": {
"defaultMessage": "Dvoufaktorové ověření"
},
"2fa.verify-enable": {
"defaultMessage": "Ověřit a zapnout"
},
"access-list": {
"defaultMessage": "seznam přístupů"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {pravidlo} few {pravidla} other {pravidel}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {uživatel} few {uživatelé} other {uživatelů}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Když existuje alespoň jedno pravidlo, toto pravidlo „zamítnout vše“ bude přidáno jako poslední"
},
"access-list.help.rules-order": {
"defaultMessage": "Upozornění: pravidla povolit a zamítnout budou uplatňována v pořadí, v jakém jsou definována."
},
"access-list.pass-auth": {
"defaultMessage": "Odeslat ověření na Upstream"
},
"access-list.public": {
"defaultMessage": "Veřejně přístupné"
},
"access-list.public.subtitle": {
"defaultMessage": "Není potřeba základní ověření"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 nebo 192.168.1.0/24 nebo 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Splnit kterékoliv"
},
"access-list.subtitle": {
"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}"
},
"access-lists": {
"defaultMessage": "Seznamy přístupů"
},
"action.add": {
"defaultMessage": "Přidat"
},
"action.add-location": {
"defaultMessage": "Přidat umístění"
},
"action.allow": {
"defaultMessage": "Povolit"
},
"action.close": {
"defaultMessage": "Zavřít"
},
"action.delete": {
"defaultMessage": "Smazat"
},
"action.deny": {
"defaultMessage": "Zamítnout"
},
"action.disable": {
"defaultMessage": "Deaktivovat"
},
"action.download": {
"defaultMessage": "Stáhnout"
},
"action.edit": {
"defaultMessage": "Upravit"
},
"action.enable": {
"defaultMessage": "Aktivovat"
},
"action.permissions": {
"defaultMessage": "Oprávnění"
},
"action.renew": {
"defaultMessage": "Obnovit"
},
"action.view-details": {
"defaultMessage": "Zobrazit podrobnosti"
},
"auditlogs": {
"defaultMessage": "Záznamy auditu"
},
"auto": {
"defaultMessage": "Automaticky"
},
"cancel": {
"defaultMessage": "Zrušit"
},
"certificate": {
"defaultMessage": "certifikát"
},
"certificate.custom-certificate": {
"defaultMessage": "Certifikát"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Klíč certifikátu"
},
"certificate.custom-intermediate": {
"defaultMessage": "Zprostředkovatelský certifikát"
},
"certificate.in-use": {
"defaultMessage": "Používá se"
},
"certificate.none.subtitle": {
"defaultMessage": "Není přiřazen žádný certifikát"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Tento hostitel nebude používat HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Žádný"
},
"certificate.not-in-use": {
"defaultMessage": "Nepoužívá se"
},
"certificate.renew": {
"defaultMessage": "Obnovit certifikát"
},
"certificates": {
"defaultMessage": "Certifikáty"
},
"certificates.custom": {
"defaultMessage": "Vlastní certifikát"
},
"certificates.custom.warning": {
"defaultMessage": "Soubory klíčů chráněné heslem nejsou podporovány."
},
"certificates.dns.credentials": {
"defaultMessage": "Obsah souboru s přihlašovacími údaji"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Tento doplněk vyžaduje konfigurační soubor obsahující API token nebo jiné přihlašovací údaje vašeho poskytovatele"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Tyto údaje budou uloženy v databázi a v souboru jako obyčejný text!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagace v sekundách"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Nechte prázdné pro výchozí hodnotu doplňku. Počet sekund, po které se čeká na propagaci DNS."
},
"certificates.dns.provider": {
"defaultMessage": "DNS poskytovatel"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Vyberte poskytovatele..."
},
"certificates.dns.warning": {
"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."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Váš server je dostupný a vytvoření certifikátu by mělo být možné."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Výsledky testu"
},
"certificates.http.warning": {
"defaultMessage": "Tyto domény musí být již nakonfigurovány tak, aby směřovaly na tuto instalaci."
},
"certificates.key-type": {
"defaultMessage": "Typ klíče"
},
"certificates.key-type-description": {
"defaultMessage": "RSA je široce kompatibilní, ECDSA je rychlejší a bezpečnější, ale nemusí být podporován staršími systémy"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "pomocí Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Vyžádat nový certifikát"
},
"column.access": {
"defaultMessage": "Přístup"
},
"column.authorization": {
"defaultMessage": "Autorizace"
},
"column.authorizations": {
"defaultMessage": "Autorizace"
},
"column.custom-locations": {
"defaultMessage": "Vlastní umístění"
},
"column.destination": {
"defaultMessage": "Cíl"
},
"column.details": {
"defaultMessage": "Podrobnosti"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Událost"
},
"column.expires": {
"defaultMessage": "Platnost do"
},
"column.http-code": {
"defaultMessage": "Přístup"
},
"column.incoming-port": {
"defaultMessage": "Vstupní port"
},
"column.name": {
"defaultMessage": "Název"
},
"column.protocol": {
"defaultMessage": "Protokol"
},
"column.provider": {
"defaultMessage": "Poskytovatel"
},
"column.roles": {
"defaultMessage": "Role"
},
"column.rules": {
"defaultMessage": "Pravidla"
},
"column.satisfy": {
"defaultMessage": "Splnit"
},
"column.satisfy-all": {
"defaultMessage": "Všechny"
},
"column.satisfy-any": {
"defaultMessage": "Kterékoliv"
},
"column.scheme": {
"defaultMessage": "Schéma"
},
"column.source": {
"defaultMessage": "Zdroj"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stav"
},
"created-on": {
"defaultMessage": "Vytvořeno: {date}"
},
"dashboard": {
"defaultMessage": "Panel"
},
"dead-host": {
"defaultMessage": "404 hostitel"
},
"dead-hosts": {
"defaultMessage": "404 Hostitelé"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 hostitel} few {404 hostitelé} other {404 hostitelů}}"
},
"disabled": {
"defaultMessage": "Deaktivováno"
},
"domain-names": {
"defaultMessage": "Doménová jména"
},
"domain-names.max": {
"defaultMessage": "Maximálně {count} doménových jmen"
},
"domain-names.placeholder": {
"defaultMessage": "Začněte psát pro přidání domény..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards nejsou pro tento typ povoleny"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards nejsou podporovány pro tuto certifikační autoritu"
},
"domains.force-ssl": {
"defaultMessage": "Vynutit SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS povoleno"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS pro subdomény"
},
"domains.http2-support": {
"defaultMessage": "Podpora HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Použít DNS výzvu"
},
"email-address": {
"defaultMessage": "Emailová adresa"
},
"empty-search": {
"defaultMessage": "Nebyly nalezeny žádné výsledky"
},
"empty-subtitle": {
"defaultMessage": "Proč nevytvoříte nějaký?"
},
"enabled": {
"defaultMessage": "Aktivováno"
},
"error.access.at-least-one": {
"defaultMessage": "Je vyžadována alespoň jedna autorizace nebo jedno přístupové pravidlo"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Uživatelská jména pro autorizaci musí být jedinečná"
},
"error.invalid-auth": {
"defaultMessage": "Neplatný email nebo heslo"
},
"error.invalid-domain": {
"defaultMessage": "Neplatná doména: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Neplatná emailová adresa"
},
"error.max-character-length": {
"defaultMessage": "Maximální délka je {max} znak{max, plural, one {} few {y} other {ů}}"
},
"error.max-domains": {
"defaultMessage": "Příliš mnoho domén, maximum je {max}"
},
"error.maximum": {
"defaultMessage": "Maximum je {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimální délka je {min} znak{min, plural, one {} few {y} other {ů}}"
},
"error.minimum": {
"defaultMessage": "Minimum je {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Hesla se musí shodovat"
},
"error.required": {
"defaultMessage": "Toto pole je povinné"
},
"expires.on": {
"defaultMessage": "Platnost do: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forkněte mě na GitHubu"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokovat běžné exploity"
},
"host.flags.cache-assets": {
"defaultMessage": "Uložit zdroje do mezipaměti"
},
"host.flags.preserve-path": {
"defaultMessage": "Zachovat cestu"
},
"host.flags.protocols": {
"defaultMessage": "Protokoly"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Podpora WebSockets"
},
"host.forward-port": {
"defaultMessage": "Port přesměrování"
},
"host.forward-scheme": {
"defaultMessage": "Schéma"
},
"hosts": {
"defaultMessage": "Hostitelé"
},
"http-only": {
"defaultMessage": "Pouze HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt přes DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt přes HTTP"
},
"loading": {
"defaultMessage": "Načítá se…"
},
"login.2fa-code": {
"defaultMessage": "Ověřovací kód"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Vložit kód"
},
"login.2fa-description": {
"defaultMessage": "Vložte kód z vaší ověřovací aplikace"
},
"login.2fa-title": {
"defaultMessage": "Dvoufaktorové ověření"
},
"login.2fa-verify": {
"defaultMessage": "Ověřit"
},
"login.title": {
"defaultMessage": "Přihlaste se ke svému účtu"
},
"nginx-config.label": {
"defaultMessage": "Vlastní Nginx konfigurace"
},
"nginx-config.placeholder": {
"defaultMessage": "# Zadejte vlastní Nginx konfiguraci na vlastní riziko!"
},
"no-permission-error": {
"defaultMessage": "Nemáte oprávnění k zobrazení tohoto obsahu."
},
"notfound.action": {
"defaultMessage": "Zpět na hlavní stránku"
},
"notfound.content": {
"defaultMessage": "Omlouváme se, stránka, kterou hledáte, nebyla nalezena"
},
"notfound.title": {
"defaultMessage": "Ups… Našli jste chybovou stránku"
},
"notification.error": {
"defaultMessage": "Chyba"
},
"notification.object-deleted": {
"defaultMessage": "{object} byl odstraněn"
},
"notification.object-disabled": {
"defaultMessage": "{object} byl deaktivován"
},
"notification.object-enabled": {
"defaultMessage": "{object} byl aktivován"
},
"notification.object-renewed": {
"defaultMessage": "{object} byl obnoven"
},
"notification.object-saved": {
"defaultMessage": "{object} byl uložen"
},
"notification.success": {
"defaultMessage": "Úspěch"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Přidat {object}"
},
"object.delete": {
"defaultMessage": "Smazat {object}"
},
"object.delete.content": {
"defaultMessage": "Opravdu chcete smazat tento {object}?"
},
"object.edit": {
"defaultMessage": "Upravit {object}"
},
"object.empty": {
"defaultMessage": "Nejsou {objects}"
},
"object.event.created": {
"defaultMessage": "Vytvořen {object}"
},
"object.event.deleted": {
"defaultMessage": "Smazán {object}"
},
"object.event.disabled": {
"defaultMessage": "Deaktivován {object}"
},
"object.event.enabled": {
"defaultMessage": "Aktivován {object}"
},
"object.event.renewed": {
"defaultMessage": "Obnoven {object}"
},
"object.event.updated": {
"defaultMessage": "Aktualizován {object}"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Možnosti"
},
"password": {
"defaultMessage": "Heslo"
},
"password.generate": {
"defaultMessage": "Vygenerovat náhodné heslo"
},
"password.hide": {
"defaultMessage": "Skrýt heslo"
},
"password.show": {
"defaultMessage": "Zobrazit heslo"
},
"permissions.hidden": {
"defaultMessage": "Skryté"
},
"permissions.manage": {
"defaultMessage": "Spravovat"
},
"permissions.view": {
"defaultMessage": "Pouze pro zobrazení"
},
"permissions.visibility.all": {
"defaultMessage": "Všechny položky"
},
"permissions.visibility.title": {
"defaultMessage": "Viditelnost položky"
},
"permissions.visibility.user": {
"defaultMessage": "Pouze vytvořené položky"
},
"proxy-host": {
"defaultMessage": "proxy hostitele"
},
"proxy-host.forward-host": {
"defaultMessage": "Cílový název hostitele / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy hostitelé"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {proxy hostitel} few {proxy hostitelé} other {proxy hostitelů}}"
},
"public": {
"defaultMessage": "Veřejné"
},
"redirection-host": {
"defaultMessage": "přesměrovacího hostitele"
},
"redirection-host.forward-domain": {
"defaultMessage": "Cílová doména"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP kód"
},
"redirection-hosts": {
"defaultMessage": "Přesměrovací hostitelé"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {přesměrovací hostitel} few {přesměrovací hostitelé} other {přesměrovacích hostitelů}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Více možností"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Trvale přesunuto"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Dočasně přesunuto"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Podívat se na jiné"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Dočasné přesměrování"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Trvalé přesměrování"
},
"role.admin": {
"defaultMessage": "Administrátor"
},
"role.standard-user": {
"defaultMessage": "Běžný uživatel"
},
"save": {
"defaultMessage": "Uložit"
},
"setting": {
"defaultMessage": "Nastavení"
},
"settings": {
"defaultMessage": "Nastavení"
},
"settings.default-site": {
"defaultMessage": "Výchozí stránka"
},
"settings.default-site.404": {
"defaultMessage": "Stránka 404"
},
"settings.default-site.444": {
"defaultMessage": "Bez odpovědi (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Gratulační stránka"
},
"settings.default-site.description": {
"defaultMessage": "Co zobrazit, když Nginx zachytí neznámého hostitele"
},
"settings.default-site.html": {
"defaultMessage": "Vlastní HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Přesměrovat"
},
"setup.preamble": {
"defaultMessage": "Začněte vytvořením administrátorského účtu."
},
"setup.title": {
"defaultMessage": "Vítejte!"
},
"sign-in": {
"defaultMessage": "Přihlásit se"
},
"ssl-certificate": {
"defaultMessage": "SSL certifikát"
},
"stream": {
"defaultMessage": "stream"
},
"stream.forward-host": {
"defaultMessage": "Cílový hostitel"
},
"stream.forward-host.placeholder": {
"defaultMessage": "napriklad.cz nebo 10.0.0.1 nebo 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Vstupní port"
},
"streams": {
"defaultMessage": "Streamy"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {stream} few {streamy} other {streamů}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Dostupná aktualizace: {latestVersion}"
},
"user": {
"defaultMessage": "uživatele"
},
"user.change-password": {
"defaultMessage": "Změnit heslo"
},
"user.confirm-password": {
"defaultMessage": "Potvrdit heslo"
},
"user.current-password": {
"defaultMessage": "Aktuální heslo"
},
"user.edit-profile": {
"defaultMessage": "Upravit profil"
},
"user.full-name": {
"defaultMessage": "Celé jméno"
},
"user.login-as": {
"defaultMessage": "Přihlásit se jako {name}"
},
"user.logout": {
"defaultMessage": "Odhlásit se"
},
"user.new-password": {
"defaultMessage": "Nové heslo"
},
"user.nickname": {
"defaultMessage": "Přezdívka"
},
"user.set-password": {
"defaultMessage": "Nastavit heslo"
},
"user.set-permissions": {
"defaultMessage": "Nastavit oprávnění pro {name}"
},
"user.switch-dark": {
"defaultMessage": "Přepnout na tmavý režim"
},
"user.switch-light": {
"defaultMessage": "Přepnout na světlý režim"
},
"user.two-factor": {
"defaultMessage": "Dvoufaktorové ověření"
},
"username": {
"defaultMessage": "Uživatelské jméno"
},
"users": {
"defaultMessage": "Uživatelé"
}
}
================================================
FILE: frontend/src/locale/src/de.json
================================================
{
"access-list": {
"defaultMessage": "Zugriffsliste"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regel} other {Regeln}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt."
},
"access-list.help.rules-order": {
"defaultMessage": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden."
},
"access-list.pass-auth": {
"defaultMessage": "Authentifizierung an Upstream weiterleiten"
},
"access-list.public": {
"defaultMessage": "Öffentlich"
},
"access-list.public.subtitle": {
"defaultMessage": "Keine Authentifizierung erforderlich"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfy Any"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}"
},
"access-lists": {
"defaultMessage": "Zugriffslisten"
},
"action.add": {
"defaultMessage": "Hinzufügen"
},
"action.add-location": {
"defaultMessage": "Pfad hinzufügen"
},
"action.close": {
"defaultMessage": "Schließen"
},
"action.delete": {
"defaultMessage": "Löschen"
},
"action.disable": {
"defaultMessage": "Deaktivieren"
},
"action.download": {
"defaultMessage": "Herunterladen"
},
"action.edit": {
"defaultMessage": "Bearbeiten"
},
"action.enable": {
"defaultMessage": "Aktivieren"
},
"action.permissions": {
"defaultMessage": "Berechtigungen"
},
"action.renew": {
"defaultMessage": "Erneuern"
},
"action.view-details": {
"defaultMessage": "Details anzeigen"
},
"auditlogs": {
"defaultMessage": "Protokolle"
},
"cancel": {
"defaultMessage": "Abbrechen"
},
"certificate": {
"defaultMessage": "Zertifikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Zertifikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Privater Schlüssel"
},
"certificate.custom-intermediate": {
"defaultMessage": "Zwischenzertifikat"
},
"certificate.in-use": {
"defaultMessage": "In Benutzung"
},
"certificate.none.subtitle": {
"defaultMessage": "Kein Zertifikat zugewiesen"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Dieser Host verwendet kein HTTPS."
},
"certificate.none.title": {
"defaultMessage": "Kein"
},
"certificate.not-in-use": {
"defaultMessage": "Nicht in Benutzung"
},
"certificate.renew": {
"defaultMessage": "Zertifikat erneuern"
},
"certificates": {
"defaultMessage": "Zertifikate"
},
"certificates.custom": {
"defaultMessage": "Benutzerdefiniertes Zertifikat"
},
"certificates.custom.warning": {
"defaultMessage": "Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt."
},
"certificates.dns.credentials": {
"defaultMessage": "Inhalt der Anmeldedaten-Datei"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Dieses Plugin erfordert eine Konfigurationsdatei, die einen API-Token oder andere Anmeldedaten für Ihren Anbieter enthält."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Diese Daten werden als Klartext in der Datenbank und in einer Datei gespeichert!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Wartezeit in Sekunden"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Leer lassen um die Standardwartezeit des Plugins zu nutzen"
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.warning": {
"defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Ihr Server ist erreichbar und die Erstellung von Zertifikaten sollte möglich sein."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Testergebnisse"
},
"certificates.http.warning": {
"defaultMessage": "Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen."
},
"certificates.key-type": {
"defaultMessage": "Schlüsseltyp"
},
"certificates.key-type-description": {
"defaultMessage": "RSA ist weit verbreitet, ECDSA ist schneller und sicherer, wird aber möglicherweise von älteren Systemen nicht unterstützt"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Über Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Anfordern eines neuen Zertifikates"
},
"column.access": {
"defaultMessage": "Zugriff"
},
"column.authorization": {
"defaultMessage": "Genehmigung"
},
"column.authorizations": {
"defaultMessage": "Genehmigungen"
},
"column.custom-locations": {
"defaultMessage": "Benutzerdefinierte Pfade"
},
"column.destination": {
"defaultMessage": "Ziel"
},
"column.details": {
"defaultMessage": "Details"
},
"column.email": {
"defaultMessage": "E-Mail"
},
"column.event": {
"defaultMessage": "Ereignis"
},
"column.expires": {
"defaultMessage": "Verfällt am"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Eingehender Port"
},
"column.name": {
"defaultMessage": "Name"
},
"column.protocol": {
"defaultMessage": "Protokoll"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Rollen"
},
"column.rules": {
"defaultMessage": "Regeln"
},
"column.satisfy": {
"defaultMessage": "Satisfy"
},
"column.satisfy-all": {
"defaultMessage": "Alle"
},
"column.satisfy-any": {
"defaultMessage": "Jeder"
},
"column.scheme": {
"defaultMessage": "Schema"
},
"column.source": {
"defaultMessage": "Quelle"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Erstelldatum: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Hosts"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
},
"disabled": {
"defaultMessage": "Deaktiviert"
},
"domain-names": {
"defaultMessage": "Domain Names"
},
"domain-names.max": {
"defaultMessage": "{count} Maximale Anzahl von Domainnamen"
},
"domain-names.placeholder": {
"defaultMessage": "Eintragen der Domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards sind für diesen Typ nicht zulässig."
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards werden für diese Zertifizierungsstelle nicht unterstützt."
},
"domains.force-ssl": {
"defaultMessage": "Erzwinge SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS aktiviert"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Sub-domains"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Support"
},
"domains.use-dns": {
"defaultMessage": "Nutze DNS Challenge"
},
"email-address": {
"defaultMessage": "E-Mail-Adresse"
},
"empty-search": {
"defaultMessage": "Keine Ergebnisse gefunden"
},
"empty-subtitle": {
"defaultMessage": "Warum erstellen Sie nicht eine?"
},
"enabled": {
"defaultMessage": "aktiviert"
},
"error.access.at-least-one": {
"defaultMessage": "Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich."
},
"error.access.duplicate-usernames": {
"defaultMessage": "Autorisierung Benutzernamen müssen eindeutig sein"
},
"error.invalid-auth": {
"defaultMessage": "Ungültige E-Mail-Adresse oder Passwort"
},
"error.invalid-domain": {
"defaultMessage": "Ungültige Domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Ungültige E-Mail-Adresse"
},
"error.max-character-length": {
"defaultMessage": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Zu viele Domains, maximal sind {max}"
},
"error.maximum": {
"defaultMessage": "Maximum ist {max}"
},
"error.min-character-length": {
"defaultMessage": "Die minimale Länge beträgt {min} Zeichen{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Minimum ist {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Passwörter müssen übereinstimmen"
},
"error.required": {
"defaultMessage": "Dies ist erforderlich."
},
"expires.on": {
"defaultMessage": "Ablauf am: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Gängige Exploits blockieren"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Assets"
},
"host.flags.preserve-path": {
"defaultMessage": "Pfad beibehalten"
},
"host.flags.protocols": {
"defaultMessage": "Protokole"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Support"
},
"host.forward-port": {
"defaultMessage": "Forward Port"
},
"host.forward-scheme": {
"defaultMessage": "Schema"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Laden…"
},
"login.title": {
"defaultMessage": "Anmelden"
},
"nginx-config.label": {
"defaultMessage": "Benutzerdefinierte Nginx Konfiguration"
},
"nginx-config.placeholder": {
"defaultMessage": "# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!"
},
"no-permission-error": {
"defaultMessage": "Sie haben keinen Zugriff, um dies anzuzeigen."
},
"notfound.action": {
"defaultMessage": "Take me home"
},
"notfound.content": {
"defaultMessage": "Es tut uns leid, aber die gesuchte Seite wurde nicht gefunden"
},
"notfound.title": {
"defaultMessage": "Oops… You just found an error page"
},
"notification.error": {
"defaultMessage": "Error"
},
"notification.object-deleted": {
"defaultMessage": "{object} wurde gelöscht"
},
"notification.object-disabled": {
"defaultMessage": "{object} wurde deaktiviert"
},
"notification.object-enabled": {
"defaultMessage": "{object} wurde aktiviert"
},
"notification.object-renewed": {
"defaultMessage": "{object} wurde erneuert"
},
"notification.object-saved": {
"defaultMessage": "{object} wurde gespeichert"
},
"notification.success": {
"defaultMessage": "Erfolgreich"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object} hinzufügen"
},
"object.delete": {
"defaultMessage": "{object} löschen"
},
"object.delete.content": {
"defaultMessage": "{object} wirklich löschen?"
},
"object.edit": {
"defaultMessage": "{object} bearbeiten"
},
"object.empty": {
"defaultMessage": "Keine {objects} vorhanden"
},
"object.event.created": {
"defaultMessage": "{object} erstellt"
},
"object.event.deleted": {
"defaultMessage": "{object} gelöscht"
},
"object.event.disabled": {
"defaultMessage": "{object} deaktiviert"
},
"object.event.enabled": {
"defaultMessage": "{object} aktiviert"
},
"object.event.renewed": {
"defaultMessage": "{object} erneuert"
},
"object.event.updated": {
"defaultMessage": "{object} aktualisiert"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Optionen"
},
"password": {
"defaultMessage": "Passwort"
},
"password.generate": {
"defaultMessage": "Zufälliges Passwort generieren"
},
"password.hide": {
"defaultMessage": "Passwort verstecken"
},
"password.show": {
"defaultMessage": "Passwort anzeigen"
},
"permissions.hidden": {
"defaultMessage": "Versteckt"
},
"permissions.manage": {
"defaultMessage": "Verwalten"
},
"permissions.view": {
"defaultMessage": "Nur anzeigen"
},
"permissions.visibility.all": {
"defaultMessage": "Alle Elemente"
},
"permissions.visibility.title": {
"defaultMessage": "Objektsichtbarkeit"
},
"permissions.visibility.user": {
"defaultMessage": "Nur erstellte Elemente"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Öffentlich"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Forward Domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standardbenutzer"
},
"save": {
"defaultMessage": "Speichern"
},
"setting": {
"defaultMessage": "Einstellung"
},
"settings": {
"defaultMessage": "Einstellungen"
},
"settings.default-site": {
"defaultMessage": "Standardseite"
},
"settings.default-site.404": {
"defaultMessage": "404 Page"
},
"settings.default-site.444": {
"defaultMessage": "No Response (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Willkommensseite"
},
"settings.default-site.description": {
"defaultMessage": "Was angezeigt wird, wenn der Nginx eine unbekannte Webseitenanfrage bekommt"
},
"settings.default-site.html": {
"defaultMessage": "Benutzerdefinierte HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Weiterleitung"
},
"setup.preamble": {
"defaultMessage": "Beginnen Sie mit der Erstellung Ihres Administratorkontos."
},
"setup.title": {
"defaultMessage": "Willkommen!"
},
"sign-in": {
"defaultMessage": "Login"
},
"ssl-certificate": {
"defaultMessage": "SSL-Zertifikate"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Forward Host"
},
"stream.incoming-port": {
"defaultMessage": "Incoming Port"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "User"
},
"user.change-password": {
"defaultMessage": "Passwort ändern"
},
"user.confirm-password": {
"defaultMessage": "Passwort wiederholen"
},
"user.current-password": {
"defaultMessage": "Aktuelles Passwort"
},
"user.edit-profile": {
"defaultMessage": "Profil bearbeiten"
},
"user.full-name": {
"defaultMessage": "Name"
},
"user.login-as": {
"defaultMessage": "Einloggen als {name}"
},
"user.logout": {
"defaultMessage": "Ausloggen"
},
"user.new-password": {
"defaultMessage": "Neues Password"
},
"user.nickname": {
"defaultMessage": "Nickname"
},
"user.set-password": {
"defaultMessage": "Passwort setzen"
},
"user.set-permissions": {
"defaultMessage": "Berechtigungen für {name} setzen"
},
"user.switch-dark": {
"defaultMessage": "Zum Dark Mode wechseln"
},
"user.switch-light": {
"defaultMessage": "Zum Light Mode wechseln"
},
"username": {
"defaultMessage": "Benutzername"
},
"users": {
"defaultMessage": "Benutzer"
}
}
================================================
FILE: frontend/src/locale/src/en.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Backup codes remaining: {count}"
},
"2fa.backup-warning": {
"defaultMessage": "Save these backup codes in a secure place. Each code can only be used once."
},
"2fa.disable": {
"defaultMessage": "Disable Two-Factor Authentication"
},
"2fa.disable-confirm": {
"defaultMessage": "Disable 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Disabling two-factor authentication will make your account less secure."
},
"2fa.disabled": {
"defaultMessage": "Disabled"
},
"2fa.done": {
"defaultMessage": "I have saved my backup codes"
},
"2fa.enable": {
"defaultMessage": "Enable Two-Factor Authentication"
},
"2fa.enabled": {
"defaultMessage": "Enabled"
},
"2fa.enter-code": {
"defaultMessage": "Enter verification code"
},
"2fa.enter-code-disable": {
"defaultMessage": "Enter verification code to disable"
},
"2fa.regenerate": {
"defaultMessage": "Regenerate"
},
"2fa.regenerate-backup": {
"defaultMessage": "Regenerate Backup Codes"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Enter a verification code to generate new backup codes. Your old codes will be invalidated."
},
"2fa.secret-key": {
"defaultMessage": "Secret Key"
},
"2fa.setup-instructions": {
"defaultMessage": "Scan this QR code with your authenticator app, or enter the secret manually."
},
"2fa.status": {
"defaultMessage": "Status"
},
"2fa.title": {
"defaultMessage": "Two-Factor Authentication"
},
"2fa.verify-enable": {
"defaultMessage": "Verify and Enable"
},
"access-list": {
"defaultMessage": "Access List"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
},
"access-list.help-rules-last": {
"defaultMessage": "When at least 1 rule exists, this deny all rule will be added last"
},
"access-list.help.rules-order": {
"defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined."
},
"access-list.pass-auth": {
"defaultMessage": "Pass Auth to Upstream"
},
"access-list.public": {
"defaultMessage": "Publicly Accessible"
},
"access-list.public.subtitle": {
"defaultMessage": "No basic auth required"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfy Any"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}"
},
"access-lists": {
"defaultMessage": "Access Lists"
},
"action.add": {
"defaultMessage": "Add"
},
"action.add-location": {
"defaultMessage": "Add Location"
},
"action.allow": {
"defaultMessage": "Allow"
},
"action.close": {
"defaultMessage": "Close"
},
"action.delete": {
"defaultMessage": "Delete"
},
"action.deny": {
"defaultMessage": "Deny"
},
"action.disable": {
"defaultMessage": "Disable"
},
"action.download": {
"defaultMessage": "Download"
},
"action.edit": {
"defaultMessage": "Edit"
},
"action.enable": {
"defaultMessage": "Enable"
},
"action.permissions": {
"defaultMessage": "Permissions"
},
"action.renew": {
"defaultMessage": "Renew"
},
"action.view-details": {
"defaultMessage": "View Details"
},
"auditlogs": {
"defaultMessage": "Audit Logs"
},
"auto": {
"defaultMessage": "Auto"
},
"cancel": {
"defaultMessage": "Cancel"
},
"certificate": {
"defaultMessage": "Certificate"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificate"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Certificate Key"
},
"certificate.custom-intermediate": {
"defaultMessage": "Intermediate Certificate"
},
"certificate.in-use": {
"defaultMessage": "In Use"
},
"certificate.none.subtitle": {
"defaultMessage": "No certificate assigned"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "This host will not use HTTPS"
},
"certificate.none.title": {
"defaultMessage": "None"
},
"certificate.not-in-use": {
"defaultMessage": "Not Used"
},
"certificate.renew": {
"defaultMessage": "Renew Certificate"
},
"certificates": {
"defaultMessage": "Certificates"
},
"certificates.custom": {
"defaultMessage": "Custom Certificate"
},
"certificates.custom.warning": {
"defaultMessage": "Key files protected with a passphrase are not supported."
},
"certificates.dns.credentials": {
"defaultMessage": "Credentials File Content"
},
"certificates.dns.credentials-note": {
"defaultMessage": "This plugin requires a configuration file containing an API token or other credentials for your provider"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "This data will be stored as plaintext in the database and in a file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagation Seconds"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation."
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Select a Provider..."
},
"certificates.dns.warning": {
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Your server is reachable and creating certificates should be possible."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Test Results"
},
"certificates.http.warning": {
"defaultMessage": "These domains must be already configured to point to this installation."
},
"certificates.key-type": {
"defaultMessage": "Key Type"
},
"certificates.key-type-description": {
"defaultMessage": "RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "with Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Request a new Certificate"
},
"column.access": {
"defaultMessage": "Access"
},
"column.authorization": {
"defaultMessage": "Authorization"
},
"column.authorizations": {
"defaultMessage": "Authorizations"
},
"column.custom-locations": {
"defaultMessage": "Custom Locations"
},
"column.destination": {
"defaultMessage": "Destination"
},
"column.details": {
"defaultMessage": "Details"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Event"
},
"column.expires": {
"defaultMessage": "Expires"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Incoming Port"
},
"column.name": {
"defaultMessage": "Name"
},
"column.protocol": {
"defaultMessage": "Protocol"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Roles"
},
"column.rules": {
"defaultMessage": "Rules"
},
"column.satisfy": {
"defaultMessage": "Satisfy"
},
"column.satisfy-all": {
"defaultMessage": "All"
},
"column.satisfy-any": {
"defaultMessage": "Any"
},
"column.scheme": {
"defaultMessage": "Scheme"
},
"column.source": {
"defaultMessage": "Source"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Created: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Hosts"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
},
"disabled": {
"defaultMessage": "Disabled"
},
"domain-names": {
"defaultMessage": "Domain Names"
},
"domain-names.max": {
"defaultMessage": "{count} domain names maximum"
},
"domain-names.placeholder": {
"defaultMessage": "Start typing to add domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards not permitted for this type"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards not supported for this CA"
},
"domains.advanced": {
"defaultMessage": "Advanced"
},
"domains.force-ssl": {
"defaultMessage": "Force SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Enabled"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Sub-domains"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Support"
},
"domains.trust-forwarded-proto": {
"defaultMessage": "Trust Upstream Forwarded Proto Headers"
},
"domains.use-dns": {
"defaultMessage": "Use DNS Challenge"
},
"email-address": {
"defaultMessage": "Email address"
},
"empty-search": {
"defaultMessage": "No results found"
},
"empty-subtitle": {
"defaultMessage": "Why don't you create one?"
},
"enabled": {
"defaultMessage": "Enabled"
},
"error.access.at-least-one": {
"defaultMessage": "Either one Authorization or one Access Rule is required"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Authorization Usernames must be unique"
},
"error.invalid-auth": {
"defaultMessage": "Invalid email or password"
},
"error.invalid-domain": {
"defaultMessage": "Invalid domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Invalid email address"
},
"error.max-character-length": {
"defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Too many domains, max is {max}"
},
"error.maximum": {
"defaultMessage": "Maximum is {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimum length is {min} character{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Minimum is {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Passwords must match"
},
"error.required": {
"defaultMessage": "This is required"
},
"expires.on": {
"defaultMessage": "Expires: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Block Common Exploits"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Assets"
},
"host.flags.preserve-path": {
"defaultMessage": "Preserve Path"
},
"host.flags.protocols": {
"defaultMessage": "Protocols"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Support"
},
"host.forward-port": {
"defaultMessage": "Forward Port"
},
"host.forward-scheme": {
"defaultMessage": "Scheme"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Loading…"
},
"login.2fa-code": {
"defaultMessage": "Verification Code"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Enter code"
},
"login.2fa-description": {
"defaultMessage": "Enter the code from your authenticator app"
},
"login.2fa-title": {
"defaultMessage": "Two-Factor Authentication"
},
"login.2fa-verify": {
"defaultMessage": "Verify"
},
"login.title": {
"defaultMessage": "Login to your account"
},
"nginx-config.label": {
"defaultMessage": "Custom Nginx Configuration"
},
"nginx-config.placeholder": {
"defaultMessage": "# Enter your custom Nginx configuration here at your own risk!"
},
"no-permission-error": {
"defaultMessage": "You do not have access to view this."
},
"notfound.action": {
"defaultMessage": "Take me home"
},
"notfound.content": {
"defaultMessage": "We are sorry but the page you are looking for was not found"
},
"notfound.title": {
"defaultMessage": "Oops… You just found an error page"
},
"notification.error": {
"defaultMessage": "Error"
},
"notification.object-deleted": {
"defaultMessage": "{object} has been deleted"
},
"notification.object-disabled": {
"defaultMessage": "{object} has been disabled"
},
"notification.object-enabled": {
"defaultMessage": "{object} has been enabled"
},
"notification.object-renewed": {
"defaultMessage": "{object} has been renewed"
},
"notification.object-saved": {
"defaultMessage": "{object} has been saved"
},
"notification.success": {
"defaultMessage": "Success"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Add {object}"
},
"object.delete": {
"defaultMessage": "Delete {object}"
},
"object.delete.content": {
"defaultMessage": "Are you sure you want to delete this {object}?"
},
"object.edit": {
"defaultMessage": "Edit {object}"
},
"object.empty": {
"defaultMessage": "There are no {objects}"
},
"object.event.created": {
"defaultMessage": "Created {object}"
},
"object.event.deleted": {
"defaultMessage": "Deleted {object}"
},
"object.event.disabled": {
"defaultMessage": "Disabled {object}"
},
"object.event.enabled": {
"defaultMessage": "Enabled {object}"
},
"object.event.renewed": {
"defaultMessage": "Renewed {object}"
},
"object.event.updated": {
"defaultMessage": "Updated {object}"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Options"
},
"password": {
"defaultMessage": "Password"
},
"password.generate": {
"defaultMessage": "Generate random password"
},
"password.hide": {
"defaultMessage": "Hide Password"
},
"password.show": {
"defaultMessage": "Show Password"
},
"permissions.hidden": {
"defaultMessage": "Hidden"
},
"permissions.manage": {
"defaultMessage": "Manage"
},
"permissions.view": {
"defaultMessage": "View Only"
},
"permissions.visibility.all": {
"defaultMessage": "All Items"
},
"permissions.visibility.title": {
"defaultMessage": "Item Visibility"
},
"permissions.visibility.user": {
"defaultMessage": "Created Items Only"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Public"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Forward Domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiple Choices"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Moved permanently"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Moved temporarily"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 See other"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Temporary redirect"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Permanent redirect"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standard User"
},
"save": {
"defaultMessage": "Save"
},
"setting": {
"defaultMessage": "Setting"
},
"settings": {
"defaultMessage": "Settings"
},
"settings.default-site": {
"defaultMessage": "Default Site"
},
"settings.default-site.404": {
"defaultMessage": "404 Page"
},
"settings.default-site.444": {
"defaultMessage": "No Response (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Congratulations Page"
},
"settings.default-site.description": {
"defaultMessage": "What to show when Nginx is hit with an unknown Host"
},
"settings.default-site.html": {
"defaultMessage": "Custom HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Redirect"
},
"setup.preamble": {
"defaultMessage": "Get started by creating your admin account."
},
"setup.title": {
"defaultMessage": "Welcome!"
},
"sign-in": {
"defaultMessage": "Sign in"
},
"ssl-certificate": {
"defaultMessage": "SSL Certificate"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Forward Host"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Incoming Port"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Update Available: {latestVersion}"
},
"user": {
"defaultMessage": "User"
},
"user.change-password": {
"defaultMessage": "Change Password"
},
"user.confirm-password": {
"defaultMessage": "Confirm Password"
},
"user.current-password": {
"defaultMessage": "Current Password"
},
"user.edit-profile": {
"defaultMessage": "Edit Profile"
},
"user.full-name": {
"defaultMessage": "Full Name"
},
"user.login-as": {
"defaultMessage": "Sign in as {name}"
},
"user.logout": {
"defaultMessage": "Logout"
},
"user.new-password": {
"defaultMessage": "New Password"
},
"user.nickname": {
"defaultMessage": "Nickname"
},
"user.set-password": {
"defaultMessage": "Set Password"
},
"user.set-permissions": {
"defaultMessage": "Set Permissions for {name}"
},
"user.switch-dark": {
"defaultMessage": "Switch to Dark mode"
},
"user.switch-light": {
"defaultMessage": "Switch to Light mode"
},
"user.two-factor": {
"defaultMessage": "Two-Factor Auth"
},
"username": {
"defaultMessage": "Username"
},
"users": {
"defaultMessage": "Users"
}
}
================================================
FILE: frontend/src/locale/src/es.json
================================================
{
"access-list": {
"defaultMessage": "Lista de Acceso"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regla} other {Reglas}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Usuario} other {Usuarios}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Cuando exista al menos 1 regla, esta regla de denegar todo se añadirá al final"
},
"access-list.help.rules-order": {
"defaultMessage": "Ten en cuenta que las directivas de permitir y denegar se aplicarán en el orden en que estén definidas."
},
"access-list.pass-auth": {
"defaultMessage": "Pasar Autenticación al Upstream"
},
"access-list.public": {
"defaultMessage": "Accesible Públicamente"
},
"access-list.public.subtitle": {
"defaultMessage": "No se requiere autenticación básica"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 o 192.168.1.0/24 o 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfacer Cualquiera"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Usuario} other {Usuarios}}, {rules} {rules, plural, one {Regla} other {Reglas}} - Creado: {date}"
},
"access-lists": {
"defaultMessage": "Listas de Acceso"
},
"action.add": {
"defaultMessage": "Añadir"
},
"action.add-location": {
"defaultMessage": "Añadir Ubicación"
},
"action.allow": {
"defaultMessage": "Permitir"
},
"action.close": {
"defaultMessage": "Cerrar"
},
"action.delete": {
"defaultMessage": "Eliminar"
},
"action.deny": {
"defaultMessage": "Denegar"
},
"action.disable": {
"defaultMessage": "Deshabilitar"
},
"action.download": {
"defaultMessage": "Descargar"
},
"action.edit": {
"defaultMessage": "Editar"
},
"action.enable": {
"defaultMessage": "Habilitar"
},
"action.permissions": {
"defaultMessage": "Permisos"
},
"action.renew": {
"defaultMessage": "Renovar"
},
"action.view-details": {
"defaultMessage": "Ver Detalles"
},
"auditlogs": {
"defaultMessage": "Registros de Auditoría"
},
"auto": {
"defaultMessage": "Auto"
},
"cancel": {
"defaultMessage": "Cancelar"
},
"certificate": {
"defaultMessage": "Certificado"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificado"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Clave del Certificado"
},
"certificate.custom-intermediate": {
"defaultMessage": "Certificado Intermedio"
},
"certificate.in-use": {
"defaultMessage": "En Uso"
},
"certificate.none.subtitle": {
"defaultMessage": "Sin certificado asignado"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Este host no usará HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Ninguno"
},
"certificate.not-in-use": {
"defaultMessage": "Sin Usar"
},
"certificate.renew": {
"defaultMessage": "Renovar Certificado"
},
"certificates": {
"defaultMessage": "Certificados"
},
"certificates.custom": {
"defaultMessage": "Certificado Personalizado"
},
"certificates.custom.warning": {
"defaultMessage": "No se admiten archivos de claves protegidos con contraseña."
},
"certificates.dns.credentials": {
"defaultMessage": "Contenido del Archivo de Credenciales"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Este plugin requiere un archivo de configuración que contenga un token de API u otras credenciales para tu proveedor"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "¡Estos datos se almacenarán como texto plano en la base de datos y en un archivo!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Segundos de Propagación"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Dejar vacío para usar el valor predeterminado del plugin. Número de segundos a esperar para la propagación DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Proveedor DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Selecciona un Proveedor..."
},
"certificates.dns.warning": {
"defaultMessage": "Esta sección requiere algunos conocimientos sobre Certbot y sus plugins DNS. Consulta la documentación de los plugins respectivos."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Tu servidor es accesible y debería ser posible crear certificados."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Resultados de la Prueba"
},
"certificates.http.warning": {
"defaultMessage": "Estos dominios ya deben estar configurados para apuntar a esta instalación."
},
"certificates.key-type": {
"defaultMessage": "Tipo de Clave"
},
"certificates.key-type-description": {
"defaultMessage": "RSA es ampliamente compatible, ECDSA es más rápido y seguro pero puede no ser compatible con sistemas antiguos"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "con Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Solicitar un nuevo Certificado"
},
"column.access": {
"defaultMessage": "Acceso"
},
"column.authorization": {
"defaultMessage": "Autorización"
},
"column.authorizations": {
"defaultMessage": "Autorizaciones"
},
"column.custom-locations": {
"defaultMessage": "Ubicaciones Personalizadas"
},
"column.destination": {
"defaultMessage": "Destino"
},
"column.details": {
"defaultMessage": "Detalles"
},
"column.email": {
"defaultMessage": "Correo Electrónico"
},
"column.event": {
"defaultMessage": "Evento"
},
"column.expires": {
"defaultMessage": "Expira"
},
"column.http-code": {
"defaultMessage": "Código HTTP"
},
"column.incoming-port": {
"defaultMessage": "Puerto de Entrada"
},
"column.name": {
"defaultMessage": "Nombre"
},
"column.protocol": {
"defaultMessage": "Protocolo"
},
"column.provider": {
"defaultMessage": "Proveedor"
},
"column.roles": {
"defaultMessage": "Roles"
},
"column.rules": {
"defaultMessage": "Reglas"
},
"column.satisfy": {
"defaultMessage": "Satisfacer"
},
"column.satisfy-all": {
"defaultMessage": "Todo"
},
"column.satisfy-any": {
"defaultMessage": "Cualquiera"
},
"column.scheme": {
"defaultMessage": "Esquema"
},
"column.source": {
"defaultMessage": "Origen"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Estado"
},
"created-on": {
"defaultMessage": "Creado: {date}"
},
"dashboard": {
"defaultMessage": "Panel de Control"
},
"dead-host": {
"defaultMessage": "Host 404"
},
"dead-hosts": {
"defaultMessage": "Hosts 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}"
},
"disabled": {
"defaultMessage": "Deshabilitado"
},
"domain-names": {
"defaultMessage": "Nombres de Dominio"
},
"domain-names.max": {
"defaultMessage": "{count} nombres de dominio como máximo"
},
"domain-names.placeholder": {
"defaultMessage": "Comienza a escribir para añadir dominio..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "No se permiten comodines para este tipo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "No se admiten comodines para esta CA"
},
"domains.force-ssl": {
"defaultMessage": "Forzar SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Habilitado"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS en Subdominios"
},
"domains.http2-support": {
"defaultMessage": "Soporte HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Usar Desafío DNS"
},
"email-address": {
"defaultMessage": "Dirección de correo electrónico"
},
"empty-search": {
"defaultMessage": "No se encontraron resultados"
},
"empty-subtitle": {
"defaultMessage": "¿Por qué no creas uno?"
},
"enabled": {
"defaultMessage": "Habilitado"
},
"error.access.at-least-one": {
"defaultMessage": "Se requiere al menos una Autorización o una Regla de Acceso"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Los nombres de usuario de autorización deben ser únicos"
},
"error.invalid-auth": {
"defaultMessage": "Correo electrónico o contraseña no válidos"
},
"error.invalid-domain": {
"defaultMessage": "Dominio no válido: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Dirección de correo electrónico no válida"
},
"error.max-character-length": {
"defaultMessage": "La longitud máxima es {max} caracter{max, plural, one {} other {es}}"
},
"error.max-domains": {
"defaultMessage": "Demasiados dominios, el máximo es {max}"
},
"error.maximum": {
"defaultMessage": "El máximo es {max}"
},
"error.min-character-length": {
"defaultMessage": "La longitud mínima es {min} caracter{min, plural, one {} other {es}}"
},
"error.minimum": {
"defaultMessage": "El mínimo es {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Las contraseñas deben coincidir"
},
"error.required": {
"defaultMessage": "Este campo es obligatorio"
},
"expires.on": {
"defaultMessage": "Expira: {date}"
},
"footer.github-fork": {
"defaultMessage": "Bifúrcame en Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Bloquear Exploits Comunes"
},
"host.flags.cache-assets": {
"defaultMessage": "Cachear Recursos"
},
"host.flags.preserve-path": {
"defaultMessage": "Preservar Ruta"
},
"host.flags.protocols": {
"defaultMessage": "Protocolos"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Soporte de Websockets"
},
"host.forward-port": {
"defaultMessage": "Puerto"
},
"host.forward-scheme": {
"defaultMessage": "Esquema"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "Solo HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt vía DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt vía HTTP"
},
"loading": {
"defaultMessage": "Cargando…"
},
"login.title": {
"defaultMessage": "Inicia sesión en tu cuenta"
},
"nginx-config.label": {
"defaultMessage": "Configuración Personalizada de Nginx"
},
"nginx-config.placeholder": {
"defaultMessage": "# ¡Introduce aquí tu configuración personalizada de Nginx bajo tu propio riesgo!"
},
"no-permission-error": {
"defaultMessage": "No tienes acceso para ver esto."
},
"notfound.action": {
"defaultMessage": "Llévame al inicio"
},
"notfound.content": {
"defaultMessage": "Lo sentimos, pero la página que buscas no fue encontrada"
},
"notfound.title": {
"defaultMessage": "Ups… Has encontrado una página de error"
},
"notification.error": {
"defaultMessage": "Error"
},
"notification.object-deleted": {
"defaultMessage": "{object} ha sido eliminado"
},
"notification.object-disabled": {
"defaultMessage": "{object} ha sido deshabilitado"
},
"notification.object-enabled": {
"defaultMessage": "{object} ha sido habilitado"
},
"notification.object-renewed": {
"defaultMessage": "{object} ha sido renovado"
},
"notification.object-saved": {
"defaultMessage": "{object} ha sido guardado"
},
"notification.success": {
"defaultMessage": "Éxito"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Añadir {object}"
},
"object.delete": {
"defaultMessage": "Eliminar {object}"
},
"object.delete.content": {
"defaultMessage": "¿Estás seguro de que quieres eliminar este {object}?"
},
"object.edit": {
"defaultMessage": "Editar {object}"
},
"object.empty": {
"defaultMessage": "No hay {objects}"
},
"object.event.created": {
"defaultMessage": "{object} Creado"
},
"object.event.deleted": {
"defaultMessage": "{object} Eliminado"
},
"object.event.disabled": {
"defaultMessage": "{object} Deshabilitado"
},
"object.event.enabled": {
"defaultMessage": "{object} Habilitado"
},
"object.event.renewed": {
"defaultMessage": "{object} Renovado"
},
"object.event.updated": {
"defaultMessage": "{object} Actualizado"
},
"offline": {
"defaultMessage": "Desconectado"
},
"online": {
"defaultMessage": "Conectado"
},
"options": {
"defaultMessage": "Opciones"
},
"password": {
"defaultMessage": "Contraseña"
},
"password.generate": {
"defaultMessage": "Generar contraseña aleatoria"
},
"password.hide": {
"defaultMessage": "Ocultar Contraseña"
},
"password.show": {
"defaultMessage": "Mostrar Contraseña"
},
"permissions.hidden": {
"defaultMessage": "Oculto"
},
"permissions.manage": {
"defaultMessage": "Gestionar"
},
"permissions.view": {
"defaultMessage": "Solo Ver"
},
"permissions.visibility.all": {
"defaultMessage": "Todos los Elementos"
},
"permissions.visibility.title": {
"defaultMessage": "Visibilidad de Elementos"
},
"permissions.visibility.user": {
"defaultMessage": "Solo Elementos Creados"
},
"proxy-host": {
"defaultMessage": "Host Proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Nombre de Host / IP de Reenvío"
},
"proxy-hosts": {
"defaultMessage": "Hosts Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Proxy} other {Hosts Proxy}}"
},
"public": {
"defaultMessage": "Público"
},
"redirection-host": {
"defaultMessage": "Host de Redirección"
},
"redirection-host.forward-domain": {
"defaultMessage": "Dominio de Reenvío"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Código HTTP"
},
"redirection-hosts": {
"defaultMessage": "Hosts de Redirección"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host de Redirección} other {Hosts de Redirección}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiples Opciones"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Movido permanentemente"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Movido temporalmente"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Ver otro"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Redirección temporal"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Redirección permanente"
},
"role.admin": {
"defaultMessage": "Administrador"
},
"role.standard-user": {
"defaultMessage": "Usuario Estándar"
},
"save": {
"defaultMessage": "Guardar"
},
"setting": {
"defaultMessage": "Configuración"
},
"settings": {
"defaultMessage": "Configuración"
},
"settings.default-site": {
"defaultMessage": "Sitio Predeterminado"
},
"settings.default-site.404": {
"defaultMessage": "Página 404"
},
"settings.default-site.444": {
"defaultMessage": "Sin Respuesta (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Página de Felicitaciones"
},
"settings.default-site.description": {
"defaultMessage": "Qué mostrar cuando Nginx recibe un Host desconocido"
},
"settings.default-site.html": {
"defaultMessage": "HTML Personalizado"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Redirigir"
},
"setup.preamble": {
"defaultMessage": "Comienza creando tu cuenta de administrador."
},
"setup.title": {
"defaultMessage": "¡Bienvenido!"
},
"sign-in": {
"defaultMessage": "Iniciar Sesión"
},
"ssl-certificate": {
"defaultMessage": "Certificado SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Host de Reenvío"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com o 10.0.0.1 o 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Puerto de Entrada"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Probar"
},
"user": {
"defaultMessage": "Usuario"
},
"user.change-password": {
"defaultMessage": "Cambiar Contraseña"
},
"user.confirm-password": {
"defaultMessage": "Confirmar Contraseña"
},
"user.current-password": {
"defaultMessage": "Contraseña Actual"
},
"user.edit-profile": {
"defaultMessage": "Editar Perfil"
},
"user.full-name": {
"defaultMessage": "Nombre Completo"
},
"user.login-as": {
"defaultMessage": "Iniciar sesión como {name}"
},
"user.logout": {
"defaultMessage": "Cerrar Sesión"
},
"user.new-password": {
"defaultMessage": "Nueva Contraseña"
},
"user.nickname": {
"defaultMessage": "Apodo"
},
"user.set-password": {
"defaultMessage": "Establecer Contraseña"
},
"user.set-permissions": {
"defaultMessage": "Establecer Permisos para {name}"
},
"user.switch-dark": {
"defaultMessage": "Cambiar a modo Oscuro"
},
"user.switch-light": {
"defaultMessage": "Cambiar a modo Claro"
},
"username": {
"defaultMessage": "Nombre de Usuario"
},
"users": {
"defaultMessage": "Usuarios"
}
}
================================================
FILE: frontend/src/locale/src/et.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Backup codes remaining: {count}"
},
"2fa.backup-warning": {
"defaultMessage": "Save these backup codes in a secure place. Each code can only be used once."
},
"2fa.disable": {
"defaultMessage": "Disable Two-Factor Authentication"
},
"2fa.disable-confirm": {
"defaultMessage": "Disable 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Disabling two-factor authentication will make your account less secure."
},
"2fa.disabled": {
"defaultMessage": "Disabled"
},
"2fa.done": {
"defaultMessage": "I have saved my backup codes"
},
"2fa.enable": {
"defaultMessage": "Enable Two-Factor Authentication"
},
"2fa.enabled": {
"defaultMessage": "Enabled"
},
"2fa.enter-code": {
"defaultMessage": "Enter verification code"
},
"2fa.enter-code-disable": {
"defaultMessage": "Enter verification code to disable"
},
"2fa.regenerate": {
"defaultMessage": "Regenerate"
},
"2fa.regenerate-backup": {
"defaultMessage": "Regenerate Backup Codes"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Enter a verification code to generate new backup codes. Your old codes will be invalidated."
},
"2fa.secret-key": {
"defaultMessage": "Secret Key"
},
"2fa.setup-instructions": {
"defaultMessage": "Scan this QR code with your authenticator app, or enter the secret manually."
},
"2fa.status": {
"defaultMessage": "Status"
},
"2fa.title": {
"defaultMessage": "Two-Factor Authentication"
},
"2fa.verify-enable": {
"defaultMessage": "Verify and Enable"
},
"access-list": {
"defaultMessage": "Access List"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
},
"access-list.help-rules-last": {
"defaultMessage": "When at least 1 rule exists, this deny all rule will be added last"
},
"access-list.help.rules-order": {
"defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined."
},
"access-list.pass-auth": {
"defaultMessage": "Pass Auth to Upstream"
},
"access-list.public": {
"defaultMessage": "Publicly Accessible"
},
"access-list.public.subtitle": {
"defaultMessage": "No basic auth required"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfy Any"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}"
},
"access-lists": {
"defaultMessage": "Access Lists"
},
"action.add": {
"defaultMessage": "Add"
},
"action.add-location": {
"defaultMessage": "Add Location"
},
"action.allow": {
"defaultMessage": "Allow"
},
"action.close": {
"defaultMessage": "Close"
},
"action.delete": {
"defaultMessage": "Delete"
},
"action.deny": {
"defaultMessage": "Deny"
},
"action.disable": {
"defaultMessage": "Disable"
},
"action.download": {
"defaultMessage": "Download"
},
"action.edit": {
"defaultMessage": "Edit"
},
"action.enable": {
"defaultMessage": "Enable"
},
"action.permissions": {
"defaultMessage": "Permissions"
},
"action.renew": {
"defaultMessage": "Renew"
},
"action.view-details": {
"defaultMessage": "View Details"
},
"auditlogs": {
"defaultMessage": "Audit Logs"
},
"auto": {
"defaultMessage": "Auto"
},
"cancel": {
"defaultMessage": "Cancel"
},
"certificate": {
"defaultMessage": "Certificate"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificate"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Certificate Key"
},
"certificate.custom-intermediate": {
"defaultMessage": "Intermediate Certificate"
},
"certificate.in-use": {
"defaultMessage": "In Use"
},
"certificate.none.subtitle": {
"defaultMessage": "No certificate assigned"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "This host will not use HTTPS"
},
"certificate.none.title": {
"defaultMessage": "None"
},
"certificate.not-in-use": {
"defaultMessage": "Not Used"
},
"certificate.renew": {
"defaultMessage": "Renew Certificate"
},
"certificates": {
"defaultMessage": "Certificates"
},
"certificates.custom": {
"defaultMessage": "Custom Certificate"
},
"certificates.custom.warning": {
"defaultMessage": "Key files protected with a passphrase are not supported."
},
"certificates.dns.credentials": {
"defaultMessage": "Credentials File Content"
},
"certificates.dns.credentials-note": {
"defaultMessage": "This plugin requires a configuration file containing an API token or other credentials for your provider"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "This data will be stored as plaintext in the database and in a file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagation Seconds"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation."
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Select a Provider..."
},
"certificates.dns.warning": {
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Your server is reachable and creating certificates should be possible."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Test Results"
},
"certificates.http.warning": {
"defaultMessage": "These domains must be already configured to point to this installation."
},
"certificates.key-type": {
"defaultMessage": "Key Type"
},
"certificates.key-type-description": {
"defaultMessage": "RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "with Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Request a new Certificate"
},
"column.access": {
"defaultMessage": "Access"
},
"column.authorization": {
"defaultMessage": "Authorization"
},
"column.authorizations": {
"defaultMessage": "Authorizations"
},
"column.custom-locations": {
"defaultMessage": "Custom Locations"
},
"column.destination": {
"defaultMessage": "Destination"
},
"column.details": {
"defaultMessage": "Details"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Event"
},
"column.expires": {
"defaultMessage": "Expires"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Incoming Port"
},
"column.name": {
"defaultMessage": "Name"
},
"column.protocol": {
"defaultMessage": "Protocol"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Roles"
},
"column.rules": {
"defaultMessage": "Rules"
},
"column.satisfy": {
"defaultMessage": "Satisfy"
},
"column.satisfy-all": {
"defaultMessage": "All"
},
"column.satisfy-any": {
"defaultMessage": "Any"
},
"column.scheme": {
"defaultMessage": "Scheme"
},
"column.source": {
"defaultMessage": "Source"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Created: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Hosts"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
},
"disabled": {
"defaultMessage": "Disabled"
},
"domain-names": {
"defaultMessage": "Domain Names"
},
"domain-names.max": {
"defaultMessage": "{count} domain names maximum"
},
"domain-names.placeholder": {
"defaultMessage": "Start typing to add domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards not permitted for this type"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards not supported for this CA"
},
"domains.advanced": {
"defaultMessage": "Advanced"
},
"domains.force-ssl": {
"defaultMessage": "Force SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Enabled"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Sub-domains"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Support"
},
"domains.trust-forwarded-proto": {
"defaultMessage": "Trust Upstream Forwarded Proto Headers"
},
"domains.use-dns": {
"defaultMessage": "Use DNS Challenge"
},
"email-address": {
"defaultMessage": "Email address"
},
"empty-search": {
"defaultMessage": "No results found"
},
"empty-subtitle": {
"defaultMessage": "Why don't you create one?"
},
"enabled": {
"defaultMessage": "Enabled"
},
"error.access.at-least-one": {
"defaultMessage": "Either one Authorization or one Access Rule is required"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Authorization Usernames must be unique"
},
"error.invalid-auth": {
"defaultMessage": "Invalid email or password"
},
"error.invalid-domain": {
"defaultMessage": "Invalid domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Invalid email address"
},
"error.max-character-length": {
"defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Too many domains, max is {max}"
},
"error.maximum": {
"defaultMessage": "Maximum is {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimum length is {min} character{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Minimum is {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Passwords must match"
},
"error.required": {
"defaultMessage": "This is required"
},
"expires.on": {
"defaultMessage": "Expires: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Block Common Exploits"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Assets"
},
"host.flags.preserve-path": {
"defaultMessage": "Preserve Path"
},
"host.flags.protocols": {
"defaultMessage": "Protocols"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Support"
},
"host.forward-port": {
"defaultMessage": "Forward Port"
},
"host.forward-scheme": {
"defaultMessage": "Scheme"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Loading…"
},
"login.2fa-code": {
"defaultMessage": "Verification Code"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Enter code"
},
"login.2fa-description": {
"defaultMessage": "Enter the code from your authenticator app"
},
"login.2fa-title": {
"defaultMessage": "Two-Factor Authentication"
},
"login.2fa-verify": {
"defaultMessage": "Verify"
},
"login.title": {
"defaultMessage": "Login to your account"
},
"nginx-config.label": {
"defaultMessage": "Custom Nginx Configuration"
},
"nginx-config.placeholder": {
"defaultMessage": "# Enter your custom Nginx configuration here at your own risk!"
},
"no-permission-error": {
"defaultMessage": "You do not have access to view this."
},
"notfound.action": {
"defaultMessage": "Take me home"
},
"notfound.content": {
"defaultMessage": "We are sorry but the page you are looking for was not found"
},
"notfound.title": {
"defaultMessage": "Oops… You just found an error page"
},
"notification.error": {
"defaultMessage": "Error"
},
"notification.object-deleted": {
"defaultMessage": "{object} has been deleted"
},
"notification.object-disabled": {
"defaultMessage": "{object} has been disabled"
},
"notification.object-enabled": {
"defaultMessage": "{object} has been enabled"
},
"notification.object-renewed": {
"defaultMessage": "{object} has been renewed"
},
"notification.object-saved": {
"defaultMessage": "{object} has been saved"
},
"notification.success": {
"defaultMessage": "Success"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Add {object}"
},
"object.delete": {
"defaultMessage": "Delete {object}"
},
"object.delete.content": {
"defaultMessage": "Are you sure you want to delete this {object}?"
},
"object.edit": {
"defaultMessage": "Edit {object}"
},
"object.empty": {
"defaultMessage": "There are no {objects}"
},
"object.event.created": {
"defaultMessage": "Created {object}"
},
"object.event.deleted": {
"defaultMessage": "Deleted {object}"
},
"object.event.disabled": {
"defaultMessage": "Disabled {object}"
},
"object.event.enabled": {
"defaultMessage": "Enabled {object}"
},
"object.event.renewed": {
"defaultMessage": "Renewed {object}"
},
"object.event.updated": {
"defaultMessage": "Updated {object}"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Options"
},
"password": {
"defaultMessage": "Password"
},
"password.generate": {
"defaultMessage": "Generate random password"
},
"password.hide": {
"defaultMessage": "Hide Password"
},
"password.show": {
"defaultMessage": "Show Password"
},
"permissions.hidden": {
"defaultMessage": "Hidden"
},
"permissions.manage": {
"defaultMessage": "Manage"
},
"permissions.view": {
"defaultMessage": "View Only"
},
"permissions.visibility.all": {
"defaultMessage": "All Items"
},
"permissions.visibility.title": {
"defaultMessage": "Item Visibility"
},
"permissions.visibility.user": {
"defaultMessage": "Created Items Only"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Public"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Forward Domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiple Choices"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Moved permanently"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Moved temporarily"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 See other"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Temporary redirect"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Permanent redirect"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standard User"
},
"save": {
"defaultMessage": "Save"
},
"setting": {
"defaultMessage": "Setting"
},
"settings": {
"defaultMessage": "Settings"
},
"settings.default-site": {
"defaultMessage": "Default Site"
},
"settings.default-site.404": {
"defaultMessage": "404 Page"
},
"settings.default-site.444": {
"defaultMessage": "No Response (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Congratulations Page"
},
"settings.default-site.description": {
"defaultMessage": "What to show when Nginx is hit with an unknown Host"
},
"settings.default-site.html": {
"defaultMessage": "Custom HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Redirect"
},
"setup.preamble": {
"defaultMessage": "Get started by creating your admin account."
},
"setup.title": {
"defaultMessage": "Welcome!"
},
"sign-in": {
"defaultMessage": "Sign in"
},
"ssl-certificate": {
"defaultMessage": "SSL Certificate"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Forward Host"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Incoming Port"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Update Available: {latestVersion}"
},
"user": {
"defaultMessage": "User"
},
"user.change-password": {
"defaultMessage": "Change Password"
},
"user.confirm-password": {
"defaultMessage": "Confirm Password"
},
"user.current-password": {
"defaultMessage": "Current Password"
},
"user.edit-profile": {
"defaultMessage": "Edit Profile"
},
"user.full-name": {
"defaultMessage": "Full Name"
},
"user.login-as": {
"defaultMessage": "Sign in as {name}"
},
"user.logout": {
"defaultMessage": "Logout"
},
"user.new-password": {
"defaultMessage": "New Password"
},
"user.nickname": {
"defaultMessage": "Nickname"
},
"user.set-password": {
"defaultMessage": "Set Password"
},
"user.set-permissions": {
"defaultMessage": "Set Permissions for {name}"
},
"user.switch-dark": {
"defaultMessage": "Switch to Dark mode"
},
"user.switch-light": {
"defaultMessage": "Switch to Light mode"
},
"user.two-factor": {
"defaultMessage": "Two-Factor Auth"
},
"username": {
"defaultMessage": "Username"
},
"users": {
"defaultMessage": "Users"
}
}
================================================
FILE: frontend/src/locale/src/fr.json
================================================
{
"access-list": {
"defaultMessage": "Liste d'accès"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}"
},
"access-list.help-rules-last": {
"defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier."
},
"access-list.help.rules-order": {
"defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies."
},
"access-list.pass-auth": {
"defaultMessage": "Transmettre l'authentification au serveur en amont"
},
"access-list.public": {
"defaultMessage": "Accessible au public"
},
"access-list.public.subtitle": {
"defaultMessage": "Aucune authentification de base requise"
},
"access-list.satisfy-any": {
"defaultMessage": "Valide n'importe quelle règle"
},
"access-list.subtitle": {
"defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}"
},
"access-lists": {
"defaultMessage": "Listes d'accès"
},
"action.add": {
"defaultMessage": "Ajouter"
},
"action.add-location": {
"defaultMessage": "Ajouter localisation"
},
"action.close": {
"defaultMessage": "Fermer"
},
"action.delete": {
"defaultMessage": "Supprimer"
},
"action.disable": {
"defaultMessage": "Désactiver"
},
"action.download": {
"defaultMessage": "Télécharger"
},
"action.edit": {
"defaultMessage": "Modifier"
},
"action.enable": {
"defaultMessage": "Activer"
},
"action.permissions": {
"defaultMessage": "Permissions"
},
"action.renew": {
"defaultMessage": "Renouveler"
},
"action.view-details": {
"defaultMessage": "Voir les Détails"
},
"auditlogs": {
"defaultMessage": "Journaux d'audit"
},
"cancel": {
"defaultMessage": "Annuler"
},
"certificate": {
"defaultMessage": "Certificat"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Clé du Certificat"
},
"certificate.custom-intermediate": {
"defaultMessage": "Certificat intermédiaire"
},
"certificate.in-use": {
"defaultMessage": "Utilisé"
},
"certificate.none.subtitle": {
"defaultMessage": "Aucun certificat assigné"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Cet hôte n'utilisera pas le HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Aucun"
},
"certificate.not-in-use": {
"defaultMessage": "Non utilisé"
},
"certificate.renew": {
"defaultMessage": "Renouveler Certificat"
},
"certificates": {
"defaultMessage": "Certificats"
},
"certificates.custom": {
"defaultMessage": "Certificat personnalisé"
},
"certificates.custom.warning": {
"defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés."
},
"certificates.dns.credentials": {
"defaultMessage": "Contenu du fichier d'identifiants"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagation Seconds"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Fournisseur DNS"
},
"certificates.dns.warning": {
"defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Résultats du test"
},
"certificates.http.warning": {
"defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation."
},
"certificates.request.subtitle": {
"defaultMessage": "avec Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Demander un nouveau certificat"
},
"column.access": {
"defaultMessage": "Accès"
},
"column.authorization": {
"defaultMessage": "Autorisation"
},
"column.authorizations": {
"defaultMessage": "Autorisations"
},
"column.custom-locations": {
"defaultMessage": "Emplacement personnalisé"
},
"column.destination": {
"defaultMessage": "Destination"
},
"column.details": {
"defaultMessage": "Détails"
},
"column.email": {
"defaultMessage": "eMail"
},
"column.event": {
"defaultMessage": "Évènement"
},
"column.expires": {
"defaultMessage": "Expire"
},
"column.http-code": {
"defaultMessage": "Code HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port entrant"
},
"column.name": {
"defaultMessage": "Nom"
},
"column.protocol": {
"defaultMessage": "Protocole"
},
"column.provider": {
"defaultMessage": "Fournisseur"
},
"column.roles": {
"defaultMessage": "Rôles"
},
"column.rules": {
"defaultMessage": "Règles"
},
"column.satisfy": {
"defaultMessage": "Valide"
},
"column.satisfy-all": {
"defaultMessage": "All"
},
"column.satisfy-any": {
"defaultMessage": "Any"
},
"column.scheme": {
"defaultMessage": "Schéma"
},
"column.source": {
"defaultMessage": "Source"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Statut"
},
"created-on": {
"defaultMessage": "Créé : {date}"
},
"dashboard": {
"defaultMessage": "Tableau de bord"
},
"dead-host": {
"defaultMessage": "Hôte 404"
},
"dead-hosts": {
"defaultMessage": "Hôtes 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}"
},
"disabled": {
"defaultMessage": "Désactivé"
},
"domain-names": {
"defaultMessage": "Noms de domaine"
},
"domain-names.max": {
"defaultMessage": "{count} noms de domaine au maximum"
},
"domain-names.placeholder": {
"defaultMessage": "Commencez à écrire pour ajouter un domaine…"
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Les Wildcards ne sont pas permises dans ce cas"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification."
},
"domains.force-ssl": {
"defaultMessage": "Forcer SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS activé"
},
"domains.hsts-subdomains": {
"defaultMessage": "Sous-domaines HSTS"
},
"domains.http2-support": {
"defaultMessage": "Prise en charge de HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Utiliser le challenge DNS"
},
"email-address": {
"defaultMessage": "Adresse eMail"
},
"empty-search": {
"defaultMessage": "Aucun résultat trouvé"
},
"empty-subtitle": {
"defaultMessage": "Pourquoi n'en créez-vous pas un ?"
},
"enabled": {
"defaultMessage": "Activé"
},
"error.access.at-least-one": {
"defaultMessage": "Une autorisation ou une règle d'accès est requise."
},
"error.access.duplicate-usernames": {
"defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques"
},
"error.invalid-auth": {
"defaultMessage": "Adresse eMail ou mot de passe invalide"
},
"error.invalid-domain": {
"defaultMessage": "Domaine invalide : {domain}"
},
"error.invalid-email": {
"defaultMessage": "Adresse eMail invalide"
},
"error.max-character-length": {
"defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Trop de domaines, le maximum est {max}"
},
"error.maximum": {
"defaultMessage": "Le maximum est {max}"
},
"error.min-character-length": {
"defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Le minimum est {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Les mots de passe doivent correspondre"
},
"error.required": {
"defaultMessage": "Ceci est obligatoire"
},
"expires.on": {
"defaultMessage": "Expire : {date}"
},
"footer.github-fork": {
"defaultMessage": "Forkez-moi sur Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Bloquer les exploits courants"
},
"host.flags.cache-assets": {
"defaultMessage": "Ressources du cache"
},
"host.flags.preserve-path": {
"defaultMessage": "Préserver le chemin"
},
"host.flags.protocols": {
"defaultMessage": "Protocoles"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Prise en charge de Websockets"
},
"host.forward-port": {
"defaultMessage": "Port de redirection"
},
"host.forward-scheme": {
"defaultMessage": "Schéma"
},
"hosts": {
"defaultMessage": "Hôtes"
},
"http-only": {
"defaultMessage": "HTTP uniquement"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Chargement…"
},
"login.title": {
"defaultMessage": "Connectez-vous à votre compte"
},
"nginx-config.label": {
"defaultMessage": "Configuration Nginx personnalisée"
},
"nginx-config.placeholder": {
"defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !"
},
"no-permission-error": {
"defaultMessage": "Vous n'avez pas la permission de voir ce contenu."
},
"notfound.action": {
"defaultMessage": "Ramenez-moi à l'accueil"
},
"notfound.content": {
"defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable"
},
"notfound.title": {
"defaultMessage": "Oops… Vous avez découvert une page d'erreur"
},
"notification.error": {
"defaultMessage": "Erreur"
},
"notification.object-deleted": {
"defaultMessage": "{object} a été supprimé"
},
"notification.object-disabled": {
"defaultMessage": "{object} a été désactivé"
},
"notification.object-enabled": {
"defaultMessage": "{object} a été activé"
},
"notification.object-renewed": {
"defaultMessage": "{object} a été renouvelé"
},
"notification.object-saved": {
"defaultMessage": "{object} a été enregistré"
},
"notification.success": {
"defaultMessage": "Réussi"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Ajouter {object}"
},
"object.delete": {
"defaultMessage": "Supprimer {object}"
},
"object.delete.content": {
"defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?"
},
"object.edit": {
"defaultMessage": "Modifier {object}"
},
"object.empty": {
"defaultMessage": "Il n'y a aucun {objects}"
},
"object.event.created": {
"defaultMessage": "{object} créé"
},
"object.event.deleted": {
"defaultMessage": "{object} supprimé"
},
"object.event.disabled": {
"defaultMessage": "{object} désactivé"
},
"object.event.enabled": {
"defaultMessage": "{object} activé"
},
"object.event.renewed": {
"defaultMessage": "{object} renouvelé"
},
"object.event.updated": {
"defaultMessage": "{object} mis à jour"
},
"offline": {
"defaultMessage": "Hors ligne"
},
"online": {
"defaultMessage": "En ligne"
},
"options": {
"defaultMessage": "Options"
},
"password": {
"defaultMessage": "Mot de passe"
},
"password.generate": {
"defaultMessage": "Générer un mot de passe aléatoire"
},
"password.hide": {
"defaultMessage": "Masquer le mot de passe"
},
"password.show": {
"defaultMessage": "Afficher le mot de passe"
},
"permissions.hidden": {
"defaultMessage": "Masquer"
},
"permissions.manage": {
"defaultMessage": "Gérer"
},
"permissions.view": {
"defaultMessage": "Voir uniquement"
},
"permissions.visibility.all": {
"defaultMessage": "Tous les éléments"
},
"permissions.visibility.title": {
"defaultMessage": "Éléments visibles"
},
"permissions.visibility.user": {
"defaultMessage": "Éléments créés uniquement"
},
"proxy-host": {
"defaultMessage": "Hôte proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Nom d'hôte de redirection / IP"
},
"proxy-hosts": {
"defaultMessage": "Hôtes proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}"
},
"public": {
"defaultMessage": "Publique"
},
"redirection-host": {
"defaultMessage": "Hôte de redirection"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domaine de redirection"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Code HTTP"
},
"redirection-hosts": {
"defaultMessage": "Hôtes de redirection"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}"
},
"role.admin": {
"defaultMessage": "Administrateur"
},
"role.standard-user": {
"defaultMessage": "Utilisateur standard"
},
"save": {
"defaultMessage": "Enregistrer"
},
"setting": {
"defaultMessage": "Paramètre"
},
"settings": {
"defaultMessage": "Paramètres"
},
"settings.default-site": {
"defaultMessage": "Site par défaut"
},
"settings.default-site.404": {
"defaultMessage": "Page 404"
},
"settings.default-site.444": {
"defaultMessage": "Aucune réponse (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Page de félicitations"
},
"settings.default-site.description": {
"defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx"
},
"settings.default-site.html": {
"defaultMessage": "HTML personnalisé"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Redirection"
},
"setup.preamble": {
"defaultMessage": "Commencez par créer votre compte administrateur."
},
"setup.title": {
"defaultMessage": "Bienvenue !"
},
"sign-in": {
"defaultMessage": "Se connecter"
},
"ssl-certificate": {
"defaultMessage": "Certificat SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Hôte destinataire"
},
"stream.incoming-port": {
"defaultMessage": "Port d'entrée"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Mise à jour disponible : {latestVersion}"
},
"user": {
"defaultMessage": "Utilisateur"
},
"user.change-password": {
"defaultMessage": "Modifier le mot de passe"
},
"user.confirm-password": {
"defaultMessage": "Confirmer le mot de passe"
},
"user.current-password": {
"defaultMessage": "Mot de passe actuel"
},
"user.edit-profile": {
"defaultMessage": "Modifier le profil"
},
"user.full-name": {
"defaultMessage": "Nom complet"
},
"user.login-as": {
"defaultMessage": "Se connecter en tant que {name}"
},
"user.logout": {
"defaultMessage": "Déconnexion"
},
"user.new-password": {
"defaultMessage": "Nouveau mot de passe"
},
"user.nickname": {
"defaultMessage": "Pseudonyme"
},
"user.set-password": {
"defaultMessage": "Définir le mot de passe"
},
"user.set-permissions": {
"defaultMessage": "Définir les autorisations pour {name}"
},
"user.switch-dark": {
"defaultMessage": "Passer au mode Sombre"
},
"user.switch-light": {
"defaultMessage": "Passer au mode Lumineux"
},
"username": {
"defaultMessage": "Nom d'utilisateur"
},
"users": {
"defaultMessage": "Utilisateurs"
}
}
================================================
FILE: frontend/src/locale/src/ga.json
================================================
{
"access-list": {
"defaultMessage": "Liosta Rochtana"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Rial} other {Rialacha}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Úsáideoir} other {Úsáideoirí}}"
},
"access-list.help-rules-last": {
"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."
},
"access-list.help.rules-order": {
"defaultMessage": "Tabhair faoi deara go gcuirfear na treoracha ceadaigh agus diúltaigh i bhfeidhm san ord a shainmhínítear iad."
},
"access-list.pass-auth": {
"defaultMessage": "Tabhair Údarú chuig an Sruth Uachtarach"
},
"access-list.public": {
"defaultMessage": "Inrochtana don Phobal"
},
"access-list.public.subtitle": {
"defaultMessage": "Níl aon údarú bunúsach ag teastáil"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 nó 192.168.1.0/24 nó 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Sásaigh Aon"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Úsáideoir} other {Úsáideoirí}}, {rules} {rules, plural, one {Riail} other {Rialacha}} - Cruthaithe: {date}"
},
"access-lists": {
"defaultMessage": "Liostaí Rochtana"
},
"action.add": {
"defaultMessage": "Cuir leis"
},
"action.add-location": {
"defaultMessage": "Cuir Suíomh leis"
},
"action.allow": {
"defaultMessage": "Ceadaigh"
},
"action.close": {
"defaultMessage": "Dún"
},
"action.delete": {
"defaultMessage": "Scrios"
},
"action.deny": {
"defaultMessage": "Diúltaigh"
},
"action.disable": {
"defaultMessage": "Díchumasaigh"
},
"action.download": {
"defaultMessage": "Íoslódáil"
},
"action.edit": {
"defaultMessage": "Cuir in Eagar"
},
"action.enable": {
"defaultMessage": "Cumasaigh"
},
"action.permissions": {
"defaultMessage": "Ceadanna"
},
"action.renew": {
"defaultMessage": "Athnuachan"
},
"action.view-details": {
"defaultMessage": "Féach Sonraí"
},
"auditlogs": {
"defaultMessage": "Logaí Iniúchta"
},
"auto": {
"defaultMessage": "Uath"
},
"cancel": {
"defaultMessage": "Cealaigh"
},
"certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Eochair Teastais"
},
"certificate.custom-intermediate": {
"defaultMessage": "Teastas Idirmheánach"
},
"certificate.in-use": {
"defaultMessage": "In Úsáid"
},
"certificate.none.subtitle": {
"defaultMessage": "Níor sannadh aon deimhniú"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Ní úsáidfidh an t-óstach seo HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Dada"
},
"certificate.not-in-use": {
"defaultMessage": "Níor Úsáideadh"
},
"certificate.renew": {
"defaultMessage": "Athnuachan an Teastais"
},
"certificates": {
"defaultMessage": "Teastais"
},
"certificates.custom": {
"defaultMessage": "Teastas Saincheaptha"
},
"certificates.custom.warning": {
"defaultMessage": "Ní thacaítear le comhaid eochair atá cosanta le frása faire."
},
"certificates.dns.credentials": {
"defaultMessage": "Ábhar Comhaid Dintiúir"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Éilíonn an breiseán seo comhad cumraíochta ina bhfuil comhartha API nó dintiúir eile do do sholáthraí."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Stórálfar an fhaisnéis seo mar théacs simplí sa bhunachar sonraí agus i gcomhad!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Soicindí Iolraithe"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Fág folamh chun luach réamhshocraithe na mbreiseán a úsáid. Líon na soicindí le fanacht le haghaidh iomadú DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Soláthraí DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Roghnaigh Soláthraí..."
},
"certificates.dns.warning": {
"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."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Theip ar sheiceáil an inrochtaineachta mar gheall ar earráid chumarsáide le site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Tá rochtain ar do fhreastalaí agus ba cheart go mbeadh sé indéanta deimhnithe a chruthú."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Torthaí Tástála"
},
"certificates.http.warning": {
"defaultMessage": "Ní mór na fearainn seo a bheith cumraithe cheana féin chun pointeáil chuig an suiteáil seo."
},
"certificates.request.subtitle": {
"defaultMessage": "le Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Iarr Teastas nua"
},
"column.access": {
"defaultMessage": "Rochtain"
},
"column.authorization": {
"defaultMessage": "Údarú"
},
"column.authorizations": {
"defaultMessage": "Údaruithe"
},
"column.custom-locations": {
"defaultMessage": "Suíomhanna Saincheaptha"
},
"column.destination": {
"defaultMessage": "Ceann Scríbe"
},
"column.details": {
"defaultMessage": "Sonraí"
},
"column.email": {
"defaultMessage": "Ríomhphost"
},
"column.event": {
"defaultMessage": "Imeacht"
},
"column.expires": {
"defaultMessage": "Éagaíonn"
},
"column.http-code": {
"defaultMessage": "Cód HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port Isteach"
},
"column.name": {
"defaultMessage": "Ainm"
},
"column.protocol": {
"defaultMessage": "Prótacal"
},
"column.provider": {
"defaultMessage": "Soláthraí"
},
"column.roles": {
"defaultMessage": "Róil"
},
"column.rules": {
"defaultMessage": "Rialacha"
},
"column.satisfy": {
"defaultMessage": "Sásamh"
},
"column.satisfy-all": {
"defaultMessage": "Gach"
},
"column.satisfy-any": {
"defaultMessage": "Aon"
},
"column.scheme": {
"defaultMessage": "Scéim"
},
"column.source": {
"defaultMessage": "Foinse"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stádas"
},
"created-on": {
"defaultMessage": "Cruthaithe: {date}"
},
"dashboard": {
"defaultMessage": "Painéal Rialaithe"
},
"dead-host": {
"defaultMessage": "Óstach 404"
},
"dead-hosts": {
"defaultMessage": "404 Óstaigh"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach 404} other {Óstaigh 404}}"
},
"disabled": {
"defaultMessage": "Míchumasaithe"
},
"domain-names": {
"defaultMessage": "Ainmneacha Fearainn"
},
"domain-names.max": {
"defaultMessage": "Uasmhéid d'ainmneacha fearainn: {count}"
},
"domain-names.placeholder": {
"defaultMessage": "Tosaigh ag clóscríobh chun fearann a chur leis..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Ní cheadaítear cártaí fiáine don chineál seo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Ní thacaítear le cártaí fiáine don ÚD seo"
},
"domains.force-ssl": {
"defaultMessage": "Fórsáil SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Cumasaithe HSTS"
},
"domains.hsts-subdomains": {
"defaultMessage": "Fo-fhearainn HSTS"
},
"domains.http2-support": {
"defaultMessage": "Tacaíocht HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Úsáid Dúshlán DNS"
},
"email-address": {
"defaultMessage": "Seoladh ríomhphoist"
},
"empty-search": {
"defaultMessage": "Níor aimsíodh aon torthaí"
},
"empty-subtitle": {
"defaultMessage": "Cén fáth nach gcruthaíonn tú ceann?"
},
"enabled": {
"defaultMessage": "Cumasaithe"
},
"error.access.at-least-one": {
"defaultMessage": "Tá Údarú amháin nó Riail Rochtana amháin ag teastáil"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Ní mór d’ainmneacha úsáideora údaraithe a bheith uathúil"
},
"error.invalid-auth": {
"defaultMessage": "Ríomhphost nó pasfhocal neamhbhailí"
},
"error.invalid-domain": {
"defaultMessage": "Fearann neamhbhailí: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Seoladh ríomhphoist neamhbhailí"
},
"error.max-character-length": {
"defaultMessage": "Is é an fad uasta ná {max} carachtar{max, plural, one {} other {anna}}"
},
"error.max-domains": {
"defaultMessage": "An iomarca fearainn, is é {max} an t-uasmhéid"
},
"error.maximum": {
"defaultMessage": "Is é {max} an t-uasmhéid"
},
"error.min-character-length": {
"defaultMessage": "Is é an fad íosta ná {min} carachtar{min, plural, one {} other {anna}}"
},
"error.minimum": {
"defaultMessage": "Is é {min} an t-íosmhéid"
},
"error.passwords-must-match": {
"defaultMessage": "Ní mór pasfhocail a bheith mar a chéile"
},
"error.required": {
"defaultMessage": "Tá sé seo riachtanach"
},
"expires.on": {
"defaultMessage": "Éagaíonn: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forc mé ar Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blocáil Easnaimh Choitianta"
},
"host.flags.cache-assets": {
"defaultMessage": "Sócmhainní Taisce"
},
"host.flags.preserve-path": {
"defaultMessage": "Cosán a Chaomhnú"
},
"host.flags.protocols": {
"defaultMessage": "Prótacail"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Tacaíocht Websockets"
},
"host.forward-port": {
"defaultMessage": "Port Ar Aghaidh"
},
"host.forward-scheme": {
"defaultMessage": "Scéim"
},
"hosts": {
"defaultMessage": "Óstaigh"
},
"http-only": {
"defaultMessage": "HTTP Amháin"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt trí DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt trí HTTP"
},
"loading": {
"defaultMessage": "Ag lódáil…"
},
"login.title": {
"defaultMessage": "Logáil isteach i do chuntas"
},
"nginx-config.label": {
"defaultMessage": "Cumraíocht Nginx Saincheaptha"
},
"nginx-config.placeholder": {
"defaultMessage": "# Cuir isteach do chumraíocht saincheaptha Nginx anseo ar do phriacal féin!"
},
"no-permission-error": {
"defaultMessage": "Níl rochtain agat chun seo a fheiceáil."
},
"notfound.action": {
"defaultMessage": "Tabhair abhaile mé"
},
"notfound.content": {
"defaultMessage": "Tá brón orainn ach níor aimsíodh an leathanach atá á lorg agat"
},
"notfound.title": {
"defaultMessage": "Úps… Fuair tú leathanach earráide díreach anois."
},
"notification.error": {
"defaultMessage": "Earráid"
},
"notification.object-deleted": {
"defaultMessage": "Scriosadh {object}"
},
"notification.object-disabled": {
"defaultMessage": "Tá {object} díchumasaithe"
},
"notification.object-enabled": {
"defaultMessage": "Tá {object} cumasaithe"
},
"notification.object-renewed": {
"defaultMessage": "Tá {object} athnuaite"
},
"notification.object-saved": {
"defaultMessage": "Tá {object} sábháilte"
},
"notification.success": {
"defaultMessage": "Rath"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Cuir {object} leis"
},
"object.delete": {
"defaultMessage": "Scrios {object}"
},
"object.delete.content": {
"defaultMessage": "An bhfuil tú cinnte gur mian leat an {object} seo a scriosadh?"
},
"object.edit": {
"defaultMessage": "Cuir in eagar {object}"
},
"object.empty": {
"defaultMessage": "Níl aon {objects} ann"
},
"object.event.created": {
"defaultMessage": "Cruthaithe {object}"
},
"object.event.deleted": {
"defaultMessage": "Scriosadh {object}"
},
"object.event.disabled": {
"defaultMessage": "Díchumasaithe {object}"
},
"object.event.enabled": {
"defaultMessage": "Cumasaithe {object}"
},
"object.event.renewed": {
"defaultMessage": "Athnuaite {object}"
},
"object.event.updated": {
"defaultMessage": "Nuashonraithe {object}"
},
"offline": {
"defaultMessage": "As líne"
},
"online": {
"defaultMessage": "Ar líne"
},
"options": {
"defaultMessage": "Roghanna"
},
"password": {
"defaultMessage": "Pasfhocal"
},
"password.generate": {
"defaultMessage": "Gin pasfhocal randamach"
},
"password.hide": {
"defaultMessage": "Folaigh Pasfhocal"
},
"password.show": {
"defaultMessage": "Taispeáin Pasfhocal"
},
"permissions.hidden": {
"defaultMessage": "I bhfolach"
},
"permissions.manage": {
"defaultMessage": "Bainistigh"
},
"permissions.view": {
"defaultMessage": "Amharc Amháin"
},
"permissions.visibility.all": {
"defaultMessage": "Gach Míreanna"
},
"permissions.visibility.title": {
"defaultMessage": "Infheictheacht Míre"
},
"permissions.visibility.user": {
"defaultMessage": "Míreanna Cruthaithe Amháin"
},
"proxy-host": {
"defaultMessage": "Óstach Seachfhreastalaí"
},
"proxy-host.forward-host": {
"defaultMessage": "Ainm Óstach / IP Ar Aghaidh"
},
"proxy-hosts": {
"defaultMessage": "Óstaigh Seachfhreastalaí"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach Seachfhreastalaí} other {Óstaigh Seachfhreastalaí}}"
},
"public": {
"defaultMessage": "Poiblí"
},
"redirection-host": {
"defaultMessage": "Óstach Athsheolta"
},
"redirection-host.forward-domain": {
"defaultMessage": "Fearann Ar Aghaidh"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Cód HTTP"
},
"redirection-hosts": {
"defaultMessage": "Óstaigh Athsheolta"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Athsheoladh Óstach} other {Athsheoladh Óstaigh}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Rogha Ilghnéitheach"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Bogtha go buan"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Bogtha go sealadach"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Féach eile"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Atreorú sealadach"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Athsheoladh buan"
},
"role.admin": {
"defaultMessage": "Riarthóir"
},
"role.standard-user": {
"defaultMessage": "Úsáideoir Caighdeánach"
},
"save": {
"defaultMessage": "Sábháil"
},
"setting": {
"defaultMessage": "Socrú"
},
"settings": {
"defaultMessage": "Socruithe"
},
"settings.default-site": {
"defaultMessage": "Suíomh Réamhshocraithe"
},
"settings.default-site.404": {
"defaultMessage": "Leathanach 404"
},
"settings.default-site.444": {
"defaultMessage": "Gan Freagra (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Leathanach Comhghairdeas"
},
"settings.default-site.description": {
"defaultMessage": "Cad atá le taispeáint nuair a bhuaileann óstach anaithnid Nginx"
},
"settings.default-site.html": {
"defaultMessage": "HTML saincheaptha"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Atreorú"
},
"setup.preamble": {
"defaultMessage": "Tosaigh trí do chuntas riarthóra a chruthú."
},
"setup.title": {
"defaultMessage": "Fáilte!"
},
"sign-in": {
"defaultMessage": "Sínigh isteach"
},
"ssl-certificate": {
"defaultMessage": "Teastas SSL"
},
"stream": {
"defaultMessage": "Sruth"
},
"stream.forward-host": {
"defaultMessage": "Óstach Ar Aghaidh"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com nó 10.0.0.1 nó 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Port Isteach"
},
"streams": {
"defaultMessage": "Sruthanna"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Sruth} other {Sruthanna}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Tástáil"
},
"update-available": {
"defaultMessage": "Nuashonrú ar Fáil: {latestVersion}"
},
"user": {
"defaultMessage": "Úsáideoir"
},
"user.change-password": {
"defaultMessage": "Athraigh Pasfhocal"
},
"user.confirm-password": {
"defaultMessage": "Deimhnigh Pasfhocal"
},
"user.current-password": {
"defaultMessage": "Pasfhocal Reatha"
},
"user.edit-profile": {
"defaultMessage": "Cuir Próifíl in Eagar"
},
"user.full-name": {
"defaultMessage": "Ainm Iomlán"
},
"user.login-as": {
"defaultMessage": "Sínigh isteach mar {name}"
},
"user.logout": {
"defaultMessage": "Logáil Amach"
},
"user.new-password": {
"defaultMessage": "Pasfhocal Nua"
},
"user.nickname": {
"defaultMessage": "Leasainm"
},
"user.set-password": {
"defaultMessage": "Socraigh Pasfhocal"
},
"user.set-permissions": {
"defaultMessage": "Socraigh Ceadanna do {name}"
},
"user.switch-dark": {
"defaultMessage": "Athraigh go Mód Dorcha"
},
"user.switch-light": {
"defaultMessage": "Athraigh go mód Solais"
},
"username": {
"defaultMessage": "Ainm úsáideora"
},
"users": {
"defaultMessage": "Úsáideoirí"
}
}
================================================
FILE: frontend/src/locale/src/hu.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Hátralévő tartalék kódok: {count}"
},
"2fa.backup-warning": {
"defaultMessage": "Mentse el ezeket a tartalék kódokat biztonságos helyre. Minden kód csak egyszer használható."
},
"2fa.disable": {
"defaultMessage": "Kétfaktoros hitelesítés letiltása"
},
"2fa.disable-confirm": {
"defaultMessage": "2FA letiltása"
},
"2fa.disable-warning": {
"defaultMessage": "A kétfaktoros hitelesítés letiltása kevésbé teszi biztonságossá a fiókját."
},
"2fa.disabled": {
"defaultMessage": "Letiltva"
},
"2fa.done": {
"defaultMessage": "Elmentettem a tartalék kódjaimat"
},
"2fa.enable": {
"defaultMessage": "Kétfaktoros hitelesítés engedélyezése"
},
"2fa.enabled": {
"defaultMessage": "Engedélyezve"
},
"2fa.enter-code": {
"defaultMessage": "Adja meg az ellenőrző kódot"
},
"2fa.enter-code-disable": {
"defaultMessage": "Adja meg az ellenőrző kódot a letiltáshoz"
},
"2fa.regenerate": {
"defaultMessage": "Újragenerálás"
},
"2fa.regenerate-backup": {
"defaultMessage": "Tartalék kódok újragenerálása"
},
"2fa.regenerate-instructions": {
"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."
},
"2fa.secret-key": {
"defaultMessage": "Titkos kulcs"
},
"2fa.setup-instructions": {
"defaultMessage": "Olvassa be ezt a QR kódot a hitelesítő alkalmazásával, vagy adja meg a titkot manuálisan."
},
"2fa.status": {
"defaultMessage": "Állapot"
},
"2fa.title": {
"defaultMessage": "Kétfaktoros hitelesítés"
},
"2fa.verify-enable": {
"defaultMessage": "Ellenőrzés és engedélyezés"
},
"access-list": {
"defaultMessage": "Hozzáférési lista"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {szabály} other {szabály}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {felhasználó} other {felhasználó}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Ha legalább 1 szabály létezik, ez a mindent tiltó szabály utolsóként lesz hozzáadva"
},
"access-list.help.rules-order": {
"defaultMessage": "Vegye figyelembe, hogy az engedélyező és tiltó direktívák a meghatározásuk sorrendjében lesznek alkalmazva."
},
"access-list.pass-auth": {
"defaultMessage": "Hitelesítés továbbítása az upstream felé"
},
"access-list.public": {
"defaultMessage": "Nyilvánosan elérhető"
},
"access-list.public.subtitle": {
"defaultMessage": "Alapszintű hitelesítés nem szükséges"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 vagy 192.168.1.0/24 vagy 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Bármely teljesítése"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {felhasználó} other {felhasználó}}, {rules} {rules, plural, one {szabály} other {szabály}} - Létrehozva: {date}"
},
"access-lists": {
"defaultMessage": "Hozzáférési listák"
},
"action.add": {
"defaultMessage": "Hozzáadás"
},
"action.add-location": {
"defaultMessage": "Útvonal hozzáadása"
},
"action.allow": {
"defaultMessage": "Engedélyezés"
},
"action.close": {
"defaultMessage": "Bezárás"
},
"action.delete": {
"defaultMessage": "Törlés"
},
"action.deny": {
"defaultMessage": "Tiltás"
},
"action.disable": {
"defaultMessage": "Letiltás"
},
"action.download": {
"defaultMessage": "Letöltés"
},
"action.edit": {
"defaultMessage": "Szerkesztés"
},
"action.enable": {
"defaultMessage": "Engedélyezés"
},
"action.permissions": {
"defaultMessage": "Engedélyek"
},
"action.renew": {
"defaultMessage": "Megújítás"
},
"action.view-details": {
"defaultMessage": "Részletek megtekintése"
},
"auditlogs": {
"defaultMessage": "Audit naplók"
},
"auto": {
"defaultMessage": "Automatikus"
},
"cancel": {
"defaultMessage": "Mégse"
},
"certificate": {
"defaultMessage": "Tanúsítvány"
},
"certificate.custom-certificate": {
"defaultMessage": "Tanúsítvány"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Tanúsítvány kulcs"
},
"certificate.custom-intermediate": {
"defaultMessage": "Köztes tanúsítvány"
},
"certificate.in-use": {
"defaultMessage": "Használatban"
},
"certificate.none.subtitle": {
"defaultMessage": "Nincs tanúsítvány hozzárendelve"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Ez a kiszolgáló nem fog HTTPS-t használni"
},
"certificate.none.title": {
"defaultMessage": "Nincs"
},
"certificate.not-in-use": {
"defaultMessage": "Nincs használatban"
},
"certificate.renew": {
"defaultMessage": "Tanúsítvány megújítása"
},
"certificates": {
"defaultMessage": "Tanúsítványok"
},
"certificates.custom": {
"defaultMessage": "Egyéni tanúsítvány"
},
"certificates.custom.warning": {
"defaultMessage": "Jelszóval védett kulcsfájlok nem támogatottak."
},
"certificates.dns.credentials": {
"defaultMessage": "Hitelesítő fájl tartalma"
},
"certificates.dns.credentials-note": {
"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"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Ezek az adatok sima szövegként lesznek tárolva az adatbázisban és egy fájlban!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagálási másodpercek"
},
"certificates.dns.propagation-seconds-note": {
"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."
},
"certificates.dns.provider": {
"defaultMessage": "DNS szolgáltató"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Válasszon szolgáltatót..."
},
"certificates.dns.warning": {
"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."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "A szerver elérhető és a tanúsítványok létrehozása lehetséges lesz."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Teszt eredmények"
},
"certificates.http.warning": {
"defaultMessage": "Ezeknek a domain-eknek már konfigurálva kell lenniük, hogy erre a telepítésre mutassanak."
},
"certificates.key-type": {
"defaultMessage": "Kulcs típus"
},
"certificates.key-type-description": {
"defaultMessage": "Az RSA széles körben kompatibilis, az ECDSA gyorsabb és biztonságosabb, de nem biztos, hogy régebbi rendszerek támogatják"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encrypt-tel"
},
"certificates.request.title": {
"defaultMessage": "Új tanúsítvány kérelmezése"
},
"column.access": {
"defaultMessage": "Hozzáférés"
},
"column.authorization": {
"defaultMessage": "Jogosultság"
},
"column.authorizations": {
"defaultMessage": "Jogosultságok"
},
"column.custom-locations": {
"defaultMessage": "Egyéni útvonalak"
},
"column.destination": {
"defaultMessage": "Cél"
},
"column.details": {
"defaultMessage": "Részletek"
},
"column.email": {
"defaultMessage": "E-mail"
},
"column.event": {
"defaultMessage": "Esemény"
},
"column.expires": {
"defaultMessage": "Lejár"
},
"column.http-code": {
"defaultMessage": "HTTP kód"
},
"column.incoming-port": {
"defaultMessage": "Bejövő port"
},
"column.name": {
"defaultMessage": "Név"
},
"column.protocol": {
"defaultMessage": "Protokoll"
},
"column.provider": {
"defaultMessage": "Szolgáltató"
},
"column.roles": {
"defaultMessage": "Szerepkörök"
},
"column.rules": {
"defaultMessage": "Szabályok"
},
"column.satisfy": {
"defaultMessage": "Teljesítés"
},
"column.satisfy-all": {
"defaultMessage": "Összes"
},
"column.satisfy-any": {
"defaultMessage": "Bármely"
},
"column.scheme": {
"defaultMessage": "Séma"
},
"column.source": {
"defaultMessage": "Forrás"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Állapot"
},
"created-on": {
"defaultMessage": "Létrehozva: {date}"
},
"dashboard": {
"defaultMessage": "Vezérlőpult"
},
"dead-host": {
"defaultMessage": "404-es Kiszolgáló"
},
"dead-hosts": {
"defaultMessage": "404-es Kiszolgálók"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404-es Kiszolgáló} other {404-es Kiszolgálók}}"
},
"disabled": {
"defaultMessage": "Letiltva"
},
"domain-names": {
"defaultMessage": "Domain nevek"
},
"domain-names.max": {
"defaultMessage": "Maximum {count} domain név"
},
"domain-names.placeholder": {
"defaultMessage": "Kezdjen el gépelni domain hozzáadásához..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Helyettesítő karakterek nem engedélyezettek ennél a típusnál"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Helyettesítő karakterek nem támogatottak ennél a CA-nál"
},
"domains.force-ssl": {
"defaultMessage": "SSL kényszerítése"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS engedélyezve"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS aldomain-ek"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 támogatás"
},
"domains.use-dns": {
"defaultMessage": "DNS Challenge használata"
},
"email-address": {
"defaultMessage": "E-mail cím"
},
"empty-search": {
"defaultMessage": "Nincs találat"
},
"empty-subtitle": {
"defaultMessage": "Miért nem hoz létre egyet?"
},
"enabled": {
"defaultMessage": "Engedélyezve"
},
"error.access.at-least-one": {
"defaultMessage": "Legalább egy jogosultság vagy egy hozzáférési szabály szükséges"
},
"error.access.duplicate-usernames": {
"defaultMessage": "A jogosultsági felhasználóneveknek egyedieknek kell lenniük"
},
"error.invalid-auth": {
"defaultMessage": "Érvénytelen e-mail vagy jelszó"
},
"error.invalid-domain": {
"defaultMessage": "Érvénytelen domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Érvénytelen e-mail cím"
},
"error.max-character-length": {
"defaultMessage": "Maximális hossz {max} karakter"
},
"error.max-domains": {
"defaultMessage": "Túl sok domain, a maximum {max}"
},
"error.maximum": {
"defaultMessage": "A maximum {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimális hossz {min} karakter"
},
"error.minimum": {
"defaultMessage": "A minimum {min}"
},
"error.passwords-must-match": {
"defaultMessage": "A jelszavaknak egyezniük kell"
},
"error.required": {
"defaultMessage": "Ez kötelező"
},
"expires.on": {
"defaultMessage": "Lejár: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork-olj a GitHub-on"
},
"host.flags.block-exploits": {
"defaultMessage": "Gyakori exploitok blokkolása"
},
"host.flags.cache-assets": {
"defaultMessage": "Erőforrások gyorsítótárazása"
},
"host.flags.preserve-path": {
"defaultMessage": "Útvonal megőrzése"
},
"host.flags.protocols": {
"defaultMessage": "Protokollok"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets támogatás"
},
"host.forward-port": {
"defaultMessage": "Továbbító port"
},
"host.forward-scheme": {
"defaultMessage": "Séma"
},
"hosts": {
"defaultMessage": "Kiszolgálók"
},
"http-only": {
"defaultMessage": "Csak HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt DNS-en keresztül"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt HTTP-n keresztül"
},
"loading": {
"defaultMessage": "Betöltés…"
},
"login.2fa-code": {
"defaultMessage": "Ellenőrző kód"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Adja meg a kódot"
},
"login.2fa-description": {
"defaultMessage": "Adja meg a kódot a hitelesítő alkalmazásából"
},
"login.2fa-title": {
"defaultMessage": "Kétfaktoros hitelesítés"
},
"login.2fa-verify": {
"defaultMessage": "Ellenőrzés"
},
"login.title": {
"defaultMessage": "Jelentkezzen be a fiókjába"
},
"nginx-config.label": {
"defaultMessage": "Egyéni Nginx konfiguráció"
},
"nginx-config.placeholder": {
"defaultMessage": "# Adja meg az egyéni Nginx konfigurációját itt, saját felelősségére!"
},
"no-permission-error": {
"defaultMessage": "Nincs jogosultsága ennek megtekintéséhez."
},
"notfound.action": {
"defaultMessage": "Vigyen haza"
},
"notfound.content": {
"defaultMessage": "Sajnáljuk, de a keresett oldal nem található"
},
"notfound.title": {
"defaultMessage": "Hoppá… Hibás oldalra talált"
},
"notification.error": {
"defaultMessage": "Hiba"
},
"notification.object-deleted": {
"defaultMessage": "{object} törölve lett"
},
"notification.object-disabled": {
"defaultMessage": "{object} letiltva lett"
},
"notification.object-enabled": {
"defaultMessage": "{object} engedélyezve lett"
},
"notification.object-renewed": {
"defaultMessage": "{object} megújítva lett"
},
"notification.object-saved": {
"defaultMessage": "{object} mentve lett"
},
"notification.success": {
"defaultMessage": "Sikeres"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object} hozzáadása"
},
"object.delete": {
"defaultMessage": "{object} törlése"
},
"object.delete.content": {
"defaultMessage": "Biztosan törölni szeretné ezt: {object}?"
},
"object.edit": {
"defaultMessage": "{object} szerkesztése"
},
"object.empty": {
"defaultMessage": "Nincsenek {objects}"
},
"object.event.created": {
"defaultMessage": "{object} létrehozva"
},
"object.event.deleted": {
"defaultMessage": "{object} törölve"
},
"object.event.disabled": {
"defaultMessage": "{object} letiltva"
},
"object.event.enabled": {
"defaultMessage": "{object} engedélyezve"
},
"object.event.renewed": {
"defaultMessage": "{object} megújítva"
},
"object.event.updated": {
"defaultMessage": "{object} frissítve"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Beállítások"
},
"password": {
"defaultMessage": "Jelszó"
},
"password.generate": {
"defaultMessage": "Véletlenszerű jelszó generálása"
},
"password.hide": {
"defaultMessage": "Jelszó elrejtése"
},
"password.show": {
"defaultMessage": "Jelszó megjelenítése"
},
"permissions.hidden": {
"defaultMessage": "Rejtett"
},
"permissions.manage": {
"defaultMessage": "Kezelés"
},
"permissions.view": {
"defaultMessage": "Csak megtekintés"
},
"permissions.visibility.all": {
"defaultMessage": "Összes elem"
},
"permissions.visibility.title": {
"defaultMessage": "Elemek láthatósága"
},
"permissions.visibility.user": {
"defaultMessage": "Csak létrehozott elemek"
},
"proxy-host": {
"defaultMessage": "Proxy Kiszolgáló"
},
"proxy-host.forward-host": {
"defaultMessage": "Továbbító hostnév / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Kiszolgálók"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Kiszolgáló} other {Proxy Kiszolgálók}}"
},
"public": {
"defaultMessage": "Nyilvános"
},
"redirection-host": {
"defaultMessage": "Átirányító Kiszolgáló"
},
"redirection-host.forward-domain": {
"defaultMessage": "Továbbító domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP kód"
},
"redirection-hosts": {
"defaultMessage": "Átirányító Kiszolgálók"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Átirányító Kiszolgáló} other {Átirányító Kiszolgálók}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Többszörös választás"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Véglegesen áthelyezve"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Ideiglenesen áthelyezve"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Lásd másik"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Ideiglenes átirányítás"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Végleges átirányítás"
},
"role.admin": {
"defaultMessage": "Adminisztrátor"
},
"role.standard-user": {
"defaultMessage": "Általános felhasználó"
},
"save": {
"defaultMessage": "Mentés"
},
"setting": {
"defaultMessage": "Beállítás"
},
"settings": {
"defaultMessage": "Beállítások"
},
"settings.default-site": {
"defaultMessage": "Alapértelmezett oldal"
},
"settings.default-site.404": {
"defaultMessage": "404-es oldal"
},
"settings.default-site.444": {
"defaultMessage": "Nincs válasz (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Gratulálunk oldal"
},
"settings.default-site.description": {
"defaultMessage": "Mit mutasson az Nginx ismeretlen Kiszolgáló esetén"
},
"settings.default-site.html": {
"defaultMessage": "Egyéni HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Átirányítás"
},
"setup.preamble": {
"defaultMessage": "Kezdje az admin fiók létrehozásával."
},
"setup.title": {
"defaultMessage": "Üdvözöljük!"
},
"sign-in": {
"defaultMessage": "Bejelentkezés"
},
"ssl-certificate": {
"defaultMessage": "SSL tanúsítvány"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Továbbító kiszolgáló"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com vagy 10.0.0.1 vagy 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Bejövő port"
},
"streams": {
"defaultMessage": "Streamek"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Teszt"
},
"update-available": {
"defaultMessage": "Frissítés elérhető: {latestVersion}"
},
"user": {
"defaultMessage": "Felhasználó"
},
"user.change-password": {
"defaultMessage": "Jelszó megváltoztatása"
},
"user.confirm-password": {
"defaultMessage": "Jelszó megerősítése"
},
"user.current-password": {
"defaultMessage": "Jelenlegi jelszó"
},
"user.edit-profile": {
"defaultMessage": "Profil szerkesztése"
},
"user.full-name": {
"defaultMessage": "Teljes név"
},
"user.login-as": {
"defaultMessage": "Bejelentkezés mint {name}"
},
"user.logout": {
"defaultMessage": "Kijelentkezés"
},
"user.new-password": {
"defaultMessage": "Új jelszó"
},
"user.nickname": {
"defaultMessage": "Becenév"
},
"user.set-password": {
"defaultMessage": "Jelszó beállítása"
},
"user.set-permissions": {
"defaultMessage": "Engedélyek beállítása {name} számára"
},
"user.switch-dark": {
"defaultMessage": "Váltás sötét módra"
},
"user.switch-light": {
"defaultMessage": "Váltás világos módra"
},
"user.two-factor": {
"defaultMessage": "Kétfaktoros hitelesítés"
},
"username": {
"defaultMessage": "Felhasználónév"
},
"users": {
"defaultMessage": "Felhasználók"
}
}
================================================
FILE: frontend/src/locale/src/id.json
================================================
{
"access-list": {
"defaultMessage": "Daftar Akses"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Aturan} other {Aturan}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Pengguna} other {Pengguna}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Jika setidaknya 1 aturan ada, aturan tolak semua ini akan ditambahkan paling akhir"
},
"access-list.help.rules-order": {
"defaultMessage": "Perhatikan bahwa direktif izinkan dan tolak akan diterapkan sesuai urutan yang didefinisikan."
},
"access-list.pass-auth": {
"defaultMessage": "Teruskan Auth ke Upstream"
},
"access-list.public": {
"defaultMessage": "Dapat Diakses Publik"
},
"access-list.public.subtitle": {
"defaultMessage": "Tidak perlu basic auth"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 atau 192.168.1.0/24 atau 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Penuhi Salah Satu"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Pengguna} other {Pengguna}}, {rules} {rules, plural, one {Aturan} other {Aturan}} - Dibuat: {date}"
},
"access-lists": {
"defaultMessage": "Daftar Akses"
},
"action.add": {
"defaultMessage": "Tambah"
},
"action.add-location": {
"defaultMessage": "Tambah Lokasi"
},
"action.allow": {
"defaultMessage": "Izinkan"
},
"action.close": {
"defaultMessage": "Tutup"
},
"action.delete": {
"defaultMessage": "Hapus"
},
"action.deny": {
"defaultMessage": "Tolak"
},
"action.disable": {
"defaultMessage": "Nonaktifkan"
},
"action.download": {
"defaultMessage": "Unduh"
},
"action.edit": {
"defaultMessage": "Edit"
},
"action.enable": {
"defaultMessage": "Aktifkan"
},
"action.permissions": {
"defaultMessage": "Izin"
},
"action.renew": {
"defaultMessage": "Perpanjang"
},
"action.view-details": {
"defaultMessage": "Lihat Detail"
},
"auditlogs": {
"defaultMessage": "Log Audit"
},
"auto": {
"defaultMessage": "Otomatis"
},
"cancel": {
"defaultMessage": "Batal"
},
"certificate": {
"defaultMessage": "Sertifikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Sertifikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Kunci Sertifikat"
},
"certificate.custom-intermediate": {
"defaultMessage": "Sertifikat Intermediate"
},
"certificate.in-use": {
"defaultMessage": "Digunakan"
},
"certificate.none.subtitle": {
"defaultMessage": "Tidak ada sertifikat yang ditetapkan"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Host ini tidak akan menggunakan HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Tidak Ada"
},
"certificate.not-in-use": {
"defaultMessage": "Tidak Digunakan"
},
"certificate.renew": {
"defaultMessage": "Perpanjang Sertifikat"
},
"certificates": {
"defaultMessage": "Sertifikat"
},
"certificates.custom": {
"defaultMessage": "Sertifikat Kustom"
},
"certificates.custom.warning": {
"defaultMessage": "Berkas kunci yang dilindungi frasa sandi tidak didukung."
},
"certificates.dns.credentials": {
"defaultMessage": "Konten File Kredensial"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Plugin ini memerlukan file konfigurasi yang berisi token API atau kredensial lain untuk penyedia Anda"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Data ini akan disimpan sebagai teks biasa di database dan dalam file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Detik Propagasi"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Biarkan kosong untuk menggunakan nilai baku plugin. Jumlah detik menunggu propagasi DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Penyedia DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Pilih Penyedia..."
},
"certificates.dns.warning": {
"defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait."
},
"certificates.http.reachability-404": {
"defaultMessage": "Ada server yang ditemukan pada domain ini tetapi tampaknya bukan Nginx Proxy Manager. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Server Anda dapat dijangkau dan pembuatan sertifikat seharusnya memungkinkan."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Hasil Uji"
},
"certificates.http.warning": {
"defaultMessage": "Domain ini harus sudah dikonfigurasi agar mengarah ke instalasi ini."
},
"certificates.request.subtitle": {
"defaultMessage": "dengan Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Minta Sertifikat Baru"
},
"column.access": {
"defaultMessage": "Akses"
},
"column.authorization": {
"defaultMessage": "Otorisasi"
},
"column.authorizations": {
"defaultMessage": "Otorisasi"
},
"column.custom-locations": {
"defaultMessage": "Lokasi Kustom"
},
"column.destination": {
"defaultMessage": "Tujuan"
},
"column.details": {
"defaultMessage": "Detail"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Peristiwa"
},
"column.expires": {
"defaultMessage": "Kedaluwarsa"
},
"column.http-code": {
"defaultMessage": "Kode HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port Masuk"
},
"column.name": {
"defaultMessage": "Nama"
},
"column.protocol": {
"defaultMessage": "Protokol"
},
"column.provider": {
"defaultMessage": "Penyedia"
},
"column.roles": {
"defaultMessage": "Peran"
},
"column.rules": {
"defaultMessage": "Aturan"
},
"column.satisfy": {
"defaultMessage": "Pemenuhan"
},
"column.satisfy-all": {
"defaultMessage": "Semua"
},
"column.satisfy-any": {
"defaultMessage": "Salah Satu"
},
"column.scheme": {
"defaultMessage": "Skema"
},
"column.source": {
"defaultMessage": "Sumber"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Dibuat: {date}"
},
"dashboard": {
"defaultMessage": "Dasbor"
},
"dead-host": {
"defaultMessage": "Host 404"
},
"dead-hosts": {
"defaultMessage": "Host 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} other {Host 404}}"
},
"disabled": {
"defaultMessage": "Nonaktif"
},
"domain-names": {
"defaultMessage": "Nama Domain"
},
"domain-names.max": {
"defaultMessage": "Maksimum {count} nama domain"
},
"domain-names.placeholder": {
"defaultMessage": "Mulai mengetik untuk menambahkan domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcard tidak diizinkan untuk tipe ini"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcard tidak didukung untuk CA ini"
},
"domains.force-ssl": {
"defaultMessage": "Paksa SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Diaktifkan"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Subdomain"
},
"domains.http2-support": {
"defaultMessage": "Dukungan HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Gunakan DNS Challenge"
},
"email-address": {
"defaultMessage": "Alamat email"
},
"empty-search": {
"defaultMessage": "Tidak ada hasil"
},
"empty-subtitle": {
"defaultMessage": "Mengapa tidak membuatnya?"
},
"enabled": {
"defaultMessage": "Aktif"
},
"error.access.at-least-one": {
"defaultMessage": "Setidaknya satu Otorisasi atau satu Aturan Akses diperlukan"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Nama pengguna otorisasi harus unik"
},
"error.invalid-auth": {
"defaultMessage": "Email atau kata sandi tidak valid"
},
"error.invalid-domain": {
"defaultMessage": "Domain tidak valid: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Alamat email tidak valid"
},
"error.max-character-length": {
"defaultMessage": "Panjang maksimum adalah {max} karakter{max, plural, one {} other {}}"
},
"error.max-domains": {
"defaultMessage": "Terlalu banyak domain, maksimum {max}"
},
"error.maximum": {
"defaultMessage": "Maksimum adalah {max}"
},
"error.min-character-length": {
"defaultMessage": "Panjang minimum adalah {min} karakter{min, plural, one {} other {}}"
},
"error.minimum": {
"defaultMessage": "Minimum adalah {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Kata sandi harus cocok"
},
"error.required": {
"defaultMessage": "Ini wajib diisi"
},
"expires.on": {
"defaultMessage": "Kedaluwarsa: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork saya di GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokir Eksploit Umum"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Aset"
},
"host.flags.preserve-path": {
"defaultMessage": "Pertahankan Path"
},
"host.flags.protocols": {
"defaultMessage": "Protokol"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Dukungan Websocket"
},
"host.forward-port": {
"defaultMessage": "Port Terusan"
},
"host.forward-scheme": {
"defaultMessage": "Skema"
},
"hosts": {
"defaultMessage": "Host"
},
"http-only": {
"defaultMessage": "HTTP Saja"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Memuat…"
},
"login.title": {
"defaultMessage": "Masuk ke akun Anda"
},
"nginx-config.label": {
"defaultMessage": "Konfigurasi Nginx Kustom"
},
"nginx-config.placeholder": {
"defaultMessage": "# Masukkan konfigurasi Nginx kustom Anda di sini dengan risiko Anda sendiri!"
},
"no-permission-error": {
"defaultMessage": "Anda tidak memiliki akses untuk melihat ini."
},
"notfound.action": {
"defaultMessage": "Bawa saya pulang"
},
"notfound.content": {
"defaultMessage": "Maaf, halaman yang Anda cari tidak ditemukan"
},
"notfound.title": {
"defaultMessage": "Ups… Anda baru saja menemukan halaman error"
},
"notification.error": {
"defaultMessage": "Kesalahan"
},
"notification.object-deleted": {
"defaultMessage": "{object} telah dihapus"
},
"notification.object-disabled": {
"defaultMessage": "{object} telah dinonaktifkan"
},
"notification.object-enabled": {
"defaultMessage": "{object} telah diaktifkan"
},
"notification.object-renewed": {
"defaultMessage": "{object} telah diperpanjang"
},
"notification.object-saved": {
"defaultMessage": "{object} telah disimpan"
},
"notification.success": {
"defaultMessage": "Berhasil"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Tambah {object}"
},
"object.delete": {
"defaultMessage": "Hapus {object}"
},
"object.delete.content": {
"defaultMessage": "Apakah Anda yakin ingin menghapus {object} ini?"
},
"object.edit": {
"defaultMessage": "Edit {object}"
},
"object.empty": {
"defaultMessage": "Tidak ada {objects}"
},
"object.event.created": {
"defaultMessage": "{object} dibuat"
},
"object.event.deleted": {
"defaultMessage": "{object} dihapus"
},
"object.event.disabled": {
"defaultMessage": "{object} dinonaktifkan"
},
"object.event.enabled": {
"defaultMessage": "{object} diaktifkan"
},
"object.event.renewed": {
"defaultMessage": "{object} diperpanjang"
},
"object.event.updated": {
"defaultMessage": "{object} diperbarui"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opsi"
},
"password": {
"defaultMessage": "Kata sandi"
},
"password.generate": {
"defaultMessage": "Buat kata sandi acak"
},
"password.hide": {
"defaultMessage": "Sembunyikan Kata Sandi"
},
"password.show": {
"defaultMessage": "Tampilkan Kata Sandi"
},
"permissions.hidden": {
"defaultMessage": "Tersembunyi"
},
"permissions.manage": {
"defaultMessage": "Kelola"
},
"permissions.view": {
"defaultMessage": "Hanya Lihat"
},
"permissions.visibility.all": {
"defaultMessage": "Semua Item"
},
"permissions.visibility.title": {
"defaultMessage": "Visibilitas Item"
},
"permissions.visibility.user": {
"defaultMessage": "Hanya Item yang Dibuat"
},
"proxy-host": {
"defaultMessage": "Host Proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Hostname / IP Terusan"
},
"proxy-hosts": {
"defaultMessage": "Host Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Proxy} other {Host Proxy}}"
},
"public": {
"defaultMessage": "Publik"
},
"redirection-host": {
"defaultMessage": "Host Pengalihan"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domain Terusan"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Kode HTTP"
},
"redirection-hosts": {
"defaultMessage": "Host Pengalihan"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Pengalihan} other {Host Pengalihan}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Banyak Pilihan"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Pindah permanen"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Pindah sementara"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Lihat lainnya"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Pengalihan sementara"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Pengalihan permanen"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Pengguna Standar"
},
"save": {
"defaultMessage": "Simpan"
},
"setting": {
"defaultMessage": "Pengaturan"
},
"settings": {
"defaultMessage": "Pengaturan"
},
"settings.default-site": {
"defaultMessage": "Situs Default"
},
"settings.default-site.404": {
"defaultMessage": "Halaman 404"
},
"settings.default-site.444": {
"defaultMessage": "Tidak Ada Respons (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Halaman Ucapan Selamat"
},
"settings.default-site.description": {
"defaultMessage": "Apa yang ditampilkan saat Nginx diakses dengan Host yang tidak dikenal"
},
"settings.default-site.html": {
"defaultMessage": "HTML Kustom"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Alihkan"
},
"setup.preamble": {
"defaultMessage": "Mulai dengan membuat akun admin Anda."
},
"setup.title": {
"defaultMessage": "Selamat datang!"
},
"sign-in": {
"defaultMessage": "Masuk"
},
"ssl-certificate": {
"defaultMessage": "Sertifikat SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Host Terusan"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com atau 10.0.0.1 atau 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Port Masuk"
},
"streams": {
"defaultMessage": "Stream"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Uji"
},
"update-available": {
"defaultMessage": "Pembaruan Tersedia: {latestVersion}"
},
"user": {
"defaultMessage": "Pengguna"
},
"user.change-password": {
"defaultMessage": "Ubah Kata Sandi"
},
"user.confirm-password": {
"defaultMessage": "Konfirmasi Kata Sandi"
},
"user.current-password": {
"defaultMessage": "Kata Sandi Saat Ini"
},
"user.edit-profile": {
"defaultMessage": "Edit Profil"
},
"user.full-name": {
"defaultMessage": "Nama Lengkap"
},
"user.login-as": {
"defaultMessage": "Masuk sebagai {name}"
},
"user.logout": {
"defaultMessage": "Keluar"
},
"user.new-password": {
"defaultMessage": "Kata Sandi Baru"
},
"user.nickname": {
"defaultMessage": "Nama Panggilan"
},
"user.set-password": {
"defaultMessage": "Atur Kata Sandi"
},
"user.set-permissions": {
"defaultMessage": "Atur Izin untuk {name}"
},
"user.switch-dark": {
"defaultMessage": "Beralih ke mode gelap"
},
"user.switch-light": {
"defaultMessage": "Beralih ke mode terang"
},
"username": {
"defaultMessage": "Nama pengguna"
},
"users": {
"defaultMessage": "Pengguna"
}
}
================================================
FILE: frontend/src/locale/src/it.json
================================================
{
"access-list": {
"defaultMessage": "Lista di Accesso"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regola} other {Regole}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Utente} other {Utenti}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Quando esiste almeno 1 regola, questa regola di negazione verrà aggiunta per ultima"
},
"access-list.help.rules-order": {
"defaultMessage": "Nota che le direttive di allow e deny saranno applicate nell'ordine in cui sono definite."
},
"access-list.pass-auth": {
"defaultMessage": "Passa Autenticazione all'Upstream"
},
"access-list.public": {
"defaultMessage": "Accessibile Pubblicamente"
},
"access-list.public.subtitle": {
"defaultMessage": "Nessuna autenticazione base richiesta"
},
"access-list.satisfy-any": {
"defaultMessage": "Soddisfa Qualsiasi"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Utente} other {Utenti}}, {rules} {rules, plural, one {Regola} other {Regole}} - Creato: {date}"
},
"access-lists": {
"defaultMessage": "Liste di Accesso"
},
"action.add": {
"defaultMessage": "Aggiungi"
},
"action.add-location": {
"defaultMessage": "Aggiungi Percorso"
},
"action.close": {
"defaultMessage": "Chiudi"
},
"action.delete": {
"defaultMessage": "Elimina"
},
"action.disable": {
"defaultMessage": "Disabilita"
},
"action.download": {
"defaultMessage": "Scarica"
},
"action.edit": {
"defaultMessage": "Modifica"
},
"action.enable": {
"defaultMessage": "Abilita"
},
"action.permissions": {
"defaultMessage": "Permessi"
},
"action.renew": {
"defaultMessage": "Rinnova"
},
"action.view-details": {
"defaultMessage": "Visualizza Dettagli"
},
"auditlogs": {
"defaultMessage": "Log di Audit"
},
"cancel": {
"defaultMessage": "Annulla"
},
"certificate": {
"defaultMessage": "Certificato"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificato"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Chiave del Certificato"
},
"certificate.custom-intermediate": {
"defaultMessage": "Certificato Intermedio"
},
"certificate.in-use": {
"defaultMessage": "In Uso"
},
"certificate.none.subtitle": {
"defaultMessage": "Nessun certificato assegnato"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Questo host non utilizzerà HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Nessuno"
},
"certificate.not-in-use": {
"defaultMessage": "Non in Uso"
},
"certificate.renew": {
"defaultMessage": "Rinnova Certificato"
},
"certificates": {
"defaultMessage": "Certificati"
},
"certificates.custom": {
"defaultMessage": "Certificato Personalizzato"
},
"certificates.custom.warning": {
"defaultMessage": "I file di chiave protetti da passphrase non sono supportati."
},
"certificates.dns.credentials": {
"defaultMessage": "Contenuto File Credenziali"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Questo plugin richiede un file di configurazione contenente un token API o altre credenziali per il tuo provider"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Questi dati saranno memorizzati in chiaro nel database e in un file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Secondi di Propagazione"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Lascia vuoto per usare il valore predefinito del plugin. Numero di secondi da attendere per la propagazione DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Provider DNS"
},
"certificates.dns.warning": {
"defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Nessun server disponibile su questo dominio. Assicurati che il dominio esista e punti all'IP corretto e che la porta 80 sia inoltrata."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Il server è raggiungibile e la creazione dei certificati è possibile."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "È stato trovato un server su questo dominio ma ha restituito dati imprevisti. È il server NPM? Controlla che il dominio punti correttamente all'IP."
},
"certificates.http.test-results": {
"defaultMessage": "Risultati Test"
},
"certificates.http.warning": {
"defaultMessage": "Questi domini devono già essere configurati per puntare a questa installazione."
},
"certificates.key-type": {
"defaultMessage": "Tipo di Chiave"
},
"certificates.key-type-description": {
"defaultMessage": "RSA è ampiamente compatibile, ECDSA è più veloce e sicuro ma potrebbe non essere supportato da sistemi più vecchi"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "con Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Richiedi un nuovo Certificato"
},
"column.access": {
"defaultMessage": "Accesso"
},
"column.authorization": {
"defaultMessage": "Autorizzazione"
},
"column.authorizations": {
"defaultMessage": "Autorizzazioni"
},
"column.custom-locations": {
"defaultMessage": "Percorsi Personalizzati"
},
"column.destination": {
"defaultMessage": "Destinazione"
},
"column.details": {
"defaultMessage": "Dettagli"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Evento"
},
"column.expires": {
"defaultMessage": "Scadenza"
},
"column.http-code": {
"defaultMessage": "Codice HTTP"
},
"column.incoming-port": {
"defaultMessage": "Porta in Ingresso"
},
"column.name": {
"defaultMessage": "Nome"
},
"column.protocol": {
"defaultMessage": "Protocollo"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Ruoli"
},
"column.rules": {
"defaultMessage": "Regole"
},
"column.satisfy": {
"defaultMessage": "Condizione"
},
"column.satisfy-all": {
"defaultMessage": "Tutte"
},
"column.satisfy-any": {
"defaultMessage": "Qualsiasi"
},
"column.scheme": {
"defaultMessage": "Schema"
},
"column.source": {
"defaultMessage": "Origine"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stato"
},
"created-on": {
"defaultMessage": "Creato: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "Host 404"
},
"dead-hosts": {
"defaultMessage": "Hosts 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}"
},
"disabled": {
"defaultMessage": "Disabilitato"
},
"domain-names": {
"defaultMessage": "Nomi di Dominio"
},
"domain-names.max": {
"defaultMessage": "Massimo {count} nomi di dominio"
},
"domain-names.placeholder": {
"defaultMessage": "Inizia a digitare per aggiungere un dominio..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcard non consentite per questo tipo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcard non supportate per questa CA"
},
"domains.force-ssl": {
"defaultMessage": "Forza SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Abilitato"
},
"domains.hsts-subdomains": {
"defaultMessage": "Sottodomini HSTS"
},
"domains.http2-support": {
"defaultMessage": "Supporto HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Usa Challenge DNS"
},
"email-address": {
"defaultMessage": "Indirizzo Email"
},
"empty-search": {
"defaultMessage": "Nessun risultato trovato"
},
"empty-subtitle": {
"defaultMessage": "Perché non ne crei uno?"
},
"enabled": {
"defaultMessage": "Abilitato"
},
"error.access.at-least-one": {
"defaultMessage": "È richiesta almeno un'Autorizzazione o una Regola di Accesso"
},
"error.access.duplicate-usernames": {
"defaultMessage": "I nomi utente devono essere unici"
},
"error.invalid-auth": {
"defaultMessage": "Email o password non validi"
},
"error.invalid-domain": {
"defaultMessage": "Dominio non valido: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Indirizzo email non valido"
},
"error.max-character-length": {
"defaultMessage": "Lunghezza massima {max} caratter{max, plural, one {e} other {i}}"
},
"error.max-domains": {
"defaultMessage": "Troppi domini, massimo {max}"
},
"error.maximum": {
"defaultMessage": "Massimo {max}"
},
"error.min-character-length": {
"defaultMessage": "Lunghezza minima {min} caratter{min, plural, one {e} other {i}}"
},
"error.minimum": {
"defaultMessage": "Minimo {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Le password devono coincidere"
},
"error.required": {
"defaultMessage": "Campo obbligatorio"
},
"expires.on": {
"defaultMessage": "Scade: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forkami su GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Blocca Exploit Comuni"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache degli Asset"
},
"host.flags.preserve-path": {
"defaultMessage": "Preserva Percorso"
},
"host.flags.protocols": {
"defaultMessage": "Protocolli"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Supporto WebSockets"
},
"host.forward-port": {
"defaultMessage": "Porta di Destinazione"
},
"host.forward-scheme": {
"defaultMessage": "Schema"
},
"hosts": {
"defaultMessage": "Host"
},
"http-only": {
"defaultMessage": "Solo HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Caricamento…"
},
"login.title": {
"defaultMessage": "Accedi al tuo account"
},
"nginx-config.label": {
"defaultMessage": "Configurazione Nginx Personalizzata"
},
"nginx-config.placeholder": {
"defaultMessage": "# Inserisci qui la configurazione Nginx personalizzata a tuo rischio!"
},
"no-permission-error": {
"defaultMessage": "Non hai accesso per visualizzare questa pagina."
},
"notfound.action": {
"defaultMessage": "Torna alla Home"
},
"notfound.content": {
"defaultMessage": "Spiacenti, la pagina richiesta non è stata trovata"
},
"notfound.title": {
"defaultMessage": "Oops… Hai trovato una pagina di errore"
},
"notification.error": {
"defaultMessage": "Errore"
},
"notification.object-deleted": {
"defaultMessage": "{object} è stato eliminato"
},
"notification.object-disabled": {
"defaultMessage": "{object} è stato disabilitato"
},
"notification.object-enabled": {
"defaultMessage": "{object} è stato abilitato"
},
"notification.object-renewed": {
"defaultMessage": "{object} è stato rinnovato"
},
"notification.object-saved": {
"defaultMessage": "{object} è stato salvato"
},
"notification.success": {
"defaultMessage": "Successo"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Aggiungi {object}"
},
"object.delete": {
"defaultMessage": "Elimina {object}"
},
"object.delete.content": {
"defaultMessage": "Sei sicuro di voler eliminare questo {object}?"
},
"object.edit": {
"defaultMessage": "Modifica {object}"
},
"object.empty": {
"defaultMessage": "Non ci sono {objects} presenti"
},
"object.event.created": {
"defaultMessage": "{object} creato"
},
"object.event.deleted": {
"defaultMessage": "{object} eliminato"
},
"object.event.disabled": {
"defaultMessage": "{object} disabilitato"
},
"object.event.enabled": {
"defaultMessage": "{object} abilitato"
},
"object.event.renewed": {
"defaultMessage": "{object} rinnovato"
},
"object.event.updated": {
"defaultMessage": "{object} aggiornato"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opzioni"
},
"password": {
"defaultMessage": "Password"
},
"password.generate": {
"defaultMessage": "Genera password casuale"
},
"password.hide": {
"defaultMessage": "Nascondi Password"
},
"password.show": {
"defaultMessage": "Mostra Password"
},
"permissions.hidden": {
"defaultMessage": "Nascosto"
},
"permissions.manage": {
"defaultMessage": "Gestisci"
},
"permissions.view": {
"defaultMessage": "Sola Lettura"
},
"permissions.visibility.all": {
"defaultMessage": "Tutti gli Elementi"
},
"permissions.visibility.title": {
"defaultMessage": "Visibilità Elementi"
},
"permissions.visibility.user": {
"defaultMessage": "Solo Elementi Creati"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Hostname / IP di Destinazione"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Proxy} other {Host Proxy}}"
},
"public": {
"defaultMessage": "Pubblico"
},
"redirection-host": {
"defaultMessage": "Host di Reindirizzamento"
},
"redirection-host.forward-domain": {
"defaultMessage": "Dominio di Destinazione"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Codice HTTP"
},
"redirection-hosts": {
"defaultMessage": "Host di Reindirizzamento"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host di Reindirizzamento} other {Host di Reindirizzamento}}"
},
"role.admin": {
"defaultMessage": "Amministratore"
},
"role.standard-user": {
"defaultMessage": "Utente Standard"
},
"save": {
"defaultMessage": "Salva"
},
"setting": {
"defaultMessage": "Impostazione"
},
"settings": {
"defaultMessage": "Impostazioni"
},
"settings.default-site": {
"defaultMessage": "Sito Predefinito"
},
"settings.default-site.404": {
"defaultMessage": "Pagina 404"
},
"settings.default-site.444": {
"defaultMessage": "Nessuna Risposta (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Pagina di Congratulazioni"
},
"settings.default-site.description": {
"defaultMessage": "Cosa mostrare quando Nginx riceve una richiesta da un host sconosciuto"
},
"settings.default-site.html": {
"defaultMessage": "HTML Personalizzato"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Reindirizza"
},
"setup.preamble": {
"defaultMessage": "Inizia creando il tuo account amministratore."
},
"setup.title": {
"defaultMessage": "Benvenuto!"
},
"sign-in": {
"defaultMessage": "Accedi"
},
"ssl-certificate": {
"defaultMessage": "Certificato SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Host di Destinazione"
},
"stream.incoming-port": {
"defaultMessage": "Porta in Ingresso"
},
"streams": {
"defaultMessage": "Stream"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Aggiornamento Disponibile: {latestVersion}"
},
"user": {
"defaultMessage": "Utente"
},
"user.change-password": {
"defaultMessage": "Cambia Password"
},
"user.confirm-password": {
"defaultMessage": "Conferma Password"
},
"user.current-password": {
"defaultMessage": "Password Attuale"
},
"user.edit-profile": {
"defaultMessage": "Modifica Profilo"
},
"user.full-name": {
"defaultMessage": "Nome Completo"
},
"user.login-as": {
"defaultMessage": "Accedi come {name}"
},
"user.logout": {
"defaultMessage": "Disconnetti"
},
"user.new-password": {
"defaultMessage": "Nuova Password"
},
"user.nickname": {
"defaultMessage": "Soprannome"
},
"user.set-password": {
"defaultMessage": "Imposta Password"
},
"user.set-permissions": {
"defaultMessage": "Imposta Permessi per {name}"
},
"user.switch-dark": {
"defaultMessage": "Passa alla modalità Scura"
},
"user.switch-light": {
"defaultMessage": "Passa alla modalità Chiara"
},
"username": {
"defaultMessage": "Nome Utente"
},
"users": {
"defaultMessage": "Utenti"
}
}
================================================
FILE: frontend/src/locale/src/ja.json
================================================
{
"access-list": {
"defaultMessage": "アクセスリスト"
},
"access-list.access-count": {
"defaultMessage": "{count} ルール"
},
"access-list.auth-count": {
"defaultMessage": "{count} ユーザー"
},
"access-list.help-rules-last": {
"defaultMessage": "少なくとも 1 つのルールが存在する場合、 他のすべてを拒否するルールが最後に追加されます"
},
"access-list.help.rules-order": {
"defaultMessage": "許可コマンドと拒否コマンドは定義された順番で適用されます"
},
"access-list.pass-auth": {
"defaultMessage": "認証情報をアップストリームに送信する"
},
"access-list.public": {
"defaultMessage": "公開されたアクセス"
},
"access-list.public.subtitle": {
"defaultMessage": "ベーシック認証を使用しません"
},
"access-list.satisfy-any": {
"defaultMessage": "いずれかを満たす"
},
"access-list.subtitle": {
"defaultMessage": "{users} ユーザー, {rules} ルール - 作成日時: {date}"
},
"access-lists": {
"defaultMessage": "アクセスリスト"
},
"action.add": {
"defaultMessage": "追加"
},
"action.add-location": {
"defaultMessage": "場所を追加"
},
"action.close": {
"defaultMessage": "閉じる"
},
"action.delete": {
"defaultMessage": "削除"
},
"action.disable": {
"defaultMessage": "無効化"
},
"action.download": {
"defaultMessage": "ダウンロード"
},
"action.edit": {
"defaultMessage": "編集"
},
"action.enable": {
"defaultMessage": "有効化"
},
"action.permissions": {
"defaultMessage": "権限"
},
"action.renew": {
"defaultMessage": "更新"
},
"action.view-details": {
"defaultMessage": "詳細"
},
"auditlogs": {
"defaultMessage": "監査ログ"
},
"cancel": {
"defaultMessage": "キャンセル"
},
"certificate": {
"defaultMessage": "証明書"
},
"certificate.custom-certificate": {
"defaultMessage": "証明書"
},
"certificate.custom-certificate-key": {
"defaultMessage": "証明書キー"
},
"certificate.custom-intermediate": {
"defaultMessage": "中間証明書"
},
"certificate.in-use": {
"defaultMessage": "使用中"
},
"certificate.none.subtitle": {
"defaultMessage": "証明書が割り当てられていません"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "このホストはHTTPSを使用しません"
},
"certificate.none.title": {
"defaultMessage": "無し"
},
"certificate.not-in-use": {
"defaultMessage": "未使用"
},
"certificate.renew": {
"defaultMessage": "証明書を更新"
},
"certificates": {
"defaultMessage": "証明書"
},
"certificates.custom": {
"defaultMessage": "カスタム証明書"
},
"certificates.custom.warning": {
"defaultMessage": "パスワードによって保護されたキーファイルはサポートされていません"
},
"certificates.dns.credentials": {
"defaultMessage": "資格情報ファイルの内容"
},
"certificates.dns.credentials-note": {
"defaultMessage": "このプラグインはプロバイダーのAPIキーか認証情報を含む設定ファイルが必要です"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "このデータはファイルとデータベースにプレーンテキストとして保存されます"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "DNS伝播時間(秒)"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "DNSの伝搬時間を秒で指定します。空にするとデフォルトの値を使用します。"
},
"certificates.dns.provider": {
"defaultMessage": "DNSプロバイダー"
},
"certificates.dns.warning": {
"defaultMessage": "このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。"
},
"certificates.http.reachability-404": {
"defaultMessage": "このドメインはNginx Proxy Managerではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。"
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました"
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "このドメインには利用可能なサーバーがありません。ドメインが存在し、NPMインスタンスのIPアドレスを指していること、必要に応じてルーターでポート80が転送されていることを確認してください。"
},
"certificates.http.reachability-ok": {
"defaultMessage": "サーバーへ到達可能であり、証明書の作成が可能です。"
},
"certificates.http.reachability-other": {
"defaultMessage": "このドメインでサーバーが見つかりましたが予期しないステータスコード {code} を返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。"
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "このドメインでサーバーが見つかりましたが予期しないデータを返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。"
},
"certificates.http.test-results": {
"defaultMessage": "テスト結果"
},
"certificates.http.warning": {
"defaultMessage": "これらのドメインは、すでにこのインストール先を指すように設定されている必要がありますあ."
},
"certificates.key-type": {
"defaultMessage": "鍵タイプ"
},
"certificates.key-type-description": {
"defaultMessage": "RSAは広く互換性があり、ECDSAはより高速で安全ですが、古いシステムではサポートされていない場合があります"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encryptを使用する"
},
"certificates.request.title": {
"defaultMessage": "新しい証明書を作成"
},
"column.access": {
"defaultMessage": "アクセス"
},
"column.authorization": {
"defaultMessage": "認証"
},
"column.authorizations": {
"defaultMessage": "認証"
},
"column.custom-locations": {
"defaultMessage": "カスタムロケーション"
},
"column.destination": {
"defaultMessage": "宛先"
},
"column.details": {
"defaultMessage": "詳細"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "イベント"
},
"column.expires": {
"defaultMessage": "期限切れ"
},
"column.http-code": {
"defaultMessage": "アクセス"
},
"column.incoming-port": {
"defaultMessage": "受信ポート"
},
"column.name": {
"defaultMessage": "名前"
},
"column.protocol": {
"defaultMessage": "プロトコル"
},
"column.provider": {
"defaultMessage": "プロバイダー"
},
"column.roles": {
"defaultMessage": "Roles"
},
"column.rules": {
"defaultMessage": "ルール"
},
"column.satisfy": {
"defaultMessage": "Satisfy"
},
"column.satisfy-all": {
"defaultMessage": "すべて"
},
"column.satisfy-any": {
"defaultMessage": "いずれか"
},
"column.scheme": {
"defaultMessage": "スキーム"
},
"column.source": {
"defaultMessage": "ソース"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "ステータス"
},
"created-on": {
"defaultMessage": "作成日時: {date}"
},
"dashboard": {
"defaultMessage": "ダッシュボード"
},
"dead-host": {
"defaultMessage": "404 ホスト"
},
"dead-hosts": {
"defaultMessage": "404 ホスト"
},
"dead-hosts.count": {
"defaultMessage": "{count} 404 ホスト"
},
"disabled": {
"defaultMessage": "無効化"
},
"domain-names": {
"defaultMessage": "ドメイン名"
},
"domain-names.max": {
"defaultMessage": "{count}のドメイン名が最大です"
},
"domain-names.placeholder": {
"defaultMessage": "追加するドメインを入力..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "ワイルドカードはこのタイプでは許可されていません"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "ワイルドカードはこのCAではサポートされていません"
},
"domains.force-ssl": {
"defaultMessage": "SSLを強制"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTSを有効化"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTSサブドメイン"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2サポート"
},
"domains.use-dns": {
"defaultMessage": "DNSチャレンジを使用"
},
"email-address": {
"defaultMessage": "Emailアドレス"
},
"empty-search": {
"defaultMessage": "見つかりませんでした"
},
"empty-subtitle": {
"defaultMessage": "作ってみましょう"
},
"enabled": {
"defaultMessage": "有効"
},
"error.access.at-least-one": {
"defaultMessage": "少なくとも一つの認証またはアクセスルールが必要です"
},
"error.access.duplicate-usernames": {
"defaultMessage": "認証のユーザー名は他と同じ名前は使用できません"
},
"error.invalid-auth": {
"defaultMessage": "無効なemailまたはパスワード"
},
"error.invalid-domain": {
"defaultMessage": "無効なドメイン: {domain}"
},
"error.invalid-email": {
"defaultMessage": "無効なemailアドレス"
},
"error.max-character-length": {
"defaultMessage": "文字数は長くとも{max}文字です"
},
"error.max-domains": {
"defaultMessage": "ドメインが多すぎます, 最大値は{max}です"
},
"error.maximum": {
"defaultMessage": "最大値は{max}です"
},
"error.min-character-length": {
"defaultMessage": "文字数は少なくとも{min}文字です"
},
"error.minimum": {
"defaultMessage": "最小値は{min}です"
},
"error.passwords-must-match": {
"defaultMessage": "パスワードは一致する必要があります"
},
"error.required": {
"defaultMessage": "必須項目です"
},
"expires.on": {
"defaultMessage": "有効期限: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
},
"host.flags.block-exploits": {
"defaultMessage": "一般的なエクスプロイトをブロックする"
},
"host.flags.cache-assets": {
"defaultMessage": "アセットをキャッシュする"
},
"host.flags.preserve-path": {
"defaultMessage": "パスワードは一致する必要があります"
},
"host.flags.protocols": {
"defaultMessage": "プロトコル"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websocketsサポート"
},
"host.forward-port": {
"defaultMessage": "転送ポート"
},
"host.forward-scheme": {
"defaultMessage": "スキーム"
},
"hosts": {
"defaultMessage": "ホスト"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Loading…"
},
"login.title": {
"defaultMessage": "アカウントにログイン"
},
"nginx-config.label": {
"defaultMessage": "カスタムNginx設定"
},
"nginx-config.placeholder": {
"defaultMessage": "# Enter your custom Nginx configuration here at your own risk!"
},
"no-permission-error": {
"defaultMessage": "これを表示する権限がありません"
},
"notfound.action": {
"defaultMessage": "ホームに戻る"
},
"notfound.content": {
"defaultMessage": "申し訳ありませんが探しているページは見つかりませんでした"
},
"notfound.title": {
"defaultMessage": "おっと... エラーページにたどり着いてしまったようです"
},
"notification.error": {
"defaultMessage": "エラー"
},
"notification.object-deleted": {
"defaultMessage": "{object}は削除されました"
},
"notification.object-disabled": {
"defaultMessage": "{object}は無効化されました"
},
"notification.object-enabled": {
"defaultMessage": "{object}は有効化されました"
},
"notification.object-renewed": {
"defaultMessage": "{object}は再作成されました"
},
"notification.object-saved": {
"defaultMessage": "{object}は保存されました"
},
"notification.success": {
"defaultMessage": "成功"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object}を追加"
},
"object.delete": {
"defaultMessage": "{object}を削除"
},
"object.delete.content": {
"defaultMessage": "本当に{object}を削除しますか?"
},
"object.edit": {
"defaultMessage": "{object}を編集"
},
"object.empty": {
"defaultMessage": "{objects}はありません"
},
"object.event.created": {
"defaultMessage": "{object}を作成済み"
},
"object.event.deleted": {
"defaultMessage": "{object}を削除済み"
},
"object.event.disabled": {
"defaultMessage": "{object}を無効化済み"
},
"object.event.enabled": {
"defaultMessage": "{object}を有効化済み"
},
"object.event.renewed": {
"defaultMessage": "{object}を再作成済み"
},
"object.event.updated": {
"defaultMessage": "{object}を更新済み"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Options"
},
"password": {
"defaultMessage": "パスワード"
},
"password.generate": {
"defaultMessage": "ランダムなパスワードを生成"
},
"password.hide": {
"defaultMessage": "パスワードを隠す"
},
"password.show": {
"defaultMessage": "パスワードを表示する"
},
"permissions.hidden": {
"defaultMessage": "非公開"
},
"permissions.manage": {
"defaultMessage": "管理"
},
"permissions.view": {
"defaultMessage": "表示のみ"
},
"permissions.visibility.all": {
"defaultMessage": "すべて"
},
"permissions.visibility.title": {
"defaultMessage": "可視性"
},
"permissions.visibility.user": {
"defaultMessage": "作成したもののみ"
},
"proxy-host": {
"defaultMessage": "プロキシホスト"
},
"proxy-host.forward-host": {
"defaultMessage": "転送ホスト名/IP"
},
"proxy-hosts": {
"defaultMessage": "プロキシホスト"
},
"proxy-hosts.count": {
"defaultMessage": "{count} プロキシホスト"
},
"public": {
"defaultMessage": "Public"
},
"redirection-host": {
"defaultMessage": "リダイレクトホスト"
},
"redirection-host.forward-domain": {
"defaultMessage": "転送ホスト"
},
"redirection-hosts": {
"defaultMessage": "リダイレクトホスト"
},
"redirection-hosts.count": {
"defaultMessage": "{count} リダイレクトホスト"
},
"role.admin": {
"defaultMessage": "管理者"
},
"role.standard-user": {
"defaultMessage": "一般ユーザー"
},
"save": {
"defaultMessage": "保存"
},
"setting": {
"defaultMessage": "設定"
},
"settings": {
"defaultMessage": "設定"
},
"settings.default-site": {
"defaultMessage": "デフォルトサイト"
},
"settings.default-site.404": {
"defaultMessage": "404ページ"
},
"settings.default-site.444": {
"defaultMessage": "返答しない (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "設定ページ"
},
"settings.default-site.description": {
"defaultMessage": "不明なホストを要求されたときにNginxが何を返すかを設定します"
},
"settings.default-site.html": {
"defaultMessage": "カスタムHTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "リダイレクト"
},
"setup.preamble": {
"defaultMessage": "管理者アカウントを作成して始めましょう"
},
"setup.title": {
"defaultMessage": "ようこそ!"
},
"sign-in": {
"defaultMessage": "サインイン"
},
"ssl-certificate": {
"defaultMessage": "SSL証明書"
},
"stream": {
"defaultMessage": "ストリーム"
},
"stream.forward-host": {
"defaultMessage": "転送ホスト"
},
"stream.incoming-port": {
"defaultMessage": "受信ポート"
},
"streams": {
"defaultMessage": "ストリーム"
},
"streams.count": {
"defaultMessage": "{count} ストリーム"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "テスト"
},
"user": {
"defaultMessage": "ユーザー"
},
"user.change-password": {
"defaultMessage": "変更するパスワード"
},
"user.confirm-password": {
"defaultMessage": "変更するパスワードを確認"
},
"user.current-password": {
"defaultMessage": "現在のパスワード"
},
"user.edit-profile": {
"defaultMessage": "プロフィールを編集"
},
"user.full-name": {
"defaultMessage": "フルネーム"
},
"user.login-as": {
"defaultMessage": "{name}としてサインイン"
},
"user.logout": {
"defaultMessage": "ログアウト"
},
"user.new-password": {
"defaultMessage": "新しいパスワード"
},
"user.nickname": {
"defaultMessage": "ニックネーム"
},
"user.set-password": {
"defaultMessage": "パスワードを設定"
},
"user.set-permissions": {
"defaultMessage": "{name}に権限を設定"
},
"user.switch-dark": {
"defaultMessage": "ダークモードに変更"
},
"user.switch-light": {
"defaultMessage": "ライトモードに変更"
},
"username": {
"defaultMessage": "ユーザー名"
},
"users": {
"defaultMessage": "ユーザー"
}
}
================================================
FILE: frontend/src/locale/src/ko.json
================================================
{
"access-list": {
"defaultMessage": "접근 정책"
},
"access-list.access-count": {
"defaultMessage": "{count}개의 정책"
},
"access-list.auth-count": {
"defaultMessage": "{count}명의 사용자"
},
"access-list.help-rules-last": {
"defaultMessage": "규칙이 하나라도 있으면 아래 ‘전체 거부’ 규칙이 마지막에 추가됩니다."
},
"access-list.help.rules-order": {
"defaultMessage": "허용/거부 규칙은 정의된 순서대로 적용됩니다."
},
"access-list.pass-auth": {
"defaultMessage": "인증 정보를 원본 서버로 전달"
},
"access-list.public": {
"defaultMessage": "누구나 접근 가능"
},
"access-list.public.subtitle": {
"defaultMessage": "기본 인증 필요 없음"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 / 192.168.1.0/24 / IPv6"
},
"access-list.satisfy-any": {
"defaultMessage": "조건 중 하나라도 충족"
},
"access-list.subtitle": {
"defaultMessage": "{users}명 {users, plural, one {사용자} other {사용자}}, {rules}개 {rules, plural, one {규칙} other {규칙}} - 생성일: {date}"
},
"access-lists": {
"defaultMessage": "접근 정책"
},
"action.add": {
"defaultMessage": "추가"
},
"action.add-location": {
"defaultMessage": "경로 추가"
},
"action.allow": {
"defaultMessage": "허용"
},
"action.close": {
"defaultMessage": "닫기"
},
"action.delete": {
"defaultMessage": "삭제"
},
"action.deny": {
"defaultMessage": "거부"
},
"action.disable": {
"defaultMessage": "비활성화"
},
"action.download": {
"defaultMessage": "다운로드"
},
"action.edit": {
"defaultMessage": "편집"
},
"action.enable": {
"defaultMessage": "활성화"
},
"action.permissions": {
"defaultMessage": "권한"
},
"action.renew": {
"defaultMessage": "갱신"
},
"action.view-details": {
"defaultMessage": "자세히 보기"
},
"auditlogs": {
"defaultMessage": "감사 로그"
},
"auto": {
"defaultMessage": "자동"
},
"cancel": {
"defaultMessage": "취소"
},
"certificate": {
"defaultMessage": "인증서"
},
"certificate.custom-certificate": {
"defaultMessage": "인증서"
},
"certificate.custom-certificate-key": {
"defaultMessage": "인증서 키"
},
"certificate.custom-intermediate": {
"defaultMessage": "중간 인증서"
},
"certificate.in-use": {
"defaultMessage": "사용 중"
},
"certificate.none.subtitle": {
"defaultMessage": "지정된 인증서 없음"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "이 호스트는 HTTPS를 사용하지 않습니다."
},
"certificate.none.title": {
"defaultMessage": "없음"
},
"certificate.not-in-use": {
"defaultMessage": "사용 안 함"
},
"certificate.renew": {
"defaultMessage": "인증서 갱신"
},
"certificates": {
"defaultMessage": "인증서"
},
"certificates.custom": {
"defaultMessage": "사용자 지정 인증서"
},
"certificates.custom.warning": {
"defaultMessage": "비밀번호로 보호된 키 파일은 지원되지 않습니다."
},
"certificates.dns.credentials": {
"defaultMessage": "DNS 자격 증명 입력"
},
"certificates.dns.credentials-note": {
"defaultMessage": "이 플러그인은 API 토큰 등이 포함된 설정 파일이 필요합니다."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "입력한 정보는 데이터베이스와 파일에 평문으로 저장됩니다."
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "DNS 전파 시간"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "비워두면 기본값을 사용합니다. DNS 전파를 기다리는 시간(초)입니다."
},
"certificates.dns.provider": {
"defaultMessage": "DNS 공급자"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "공급자를 선택하세요..."
},
"certificates.dns.warning": {
"defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요."
},
"certificates.http.reachability-404": {
"defaultMessage": "해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "해당 도메인에 접근 가능한 서버가 없습니다. 도메인이 존재하며 NPM이 실행되는 IP를 가리키고, 필요하면 라우터에서 80포트가 포워딩되어 있는지 확인하세요."
},
"certificates.http.reachability-ok": {
"defaultMessage": "서버에 정상적으로 접근할 수 있으며 인증서 발급이 가능합니다."
},
"certificates.http.reachability-other": {
"defaultMessage": "해당 도메인에서 서버가 발견되었지만 예상치 못한 상태 코드 {code}를 반환했습니다. NPM 서버가 맞는지 확인하세요."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "서버가 응답했지만 예상치 못한 데이터를 반환했습니다. NPM 서버가 맞는지 확인하세요."
},
"certificates.http.test-results": {
"defaultMessage": "테스트 결과"
},
"certificates.http.warning": {
"defaultMessage": "도메인이 이 서버를 가리키도록 설정되어 있어야 합니다."
},
"certificates.key-type": {
"defaultMessage": "키 유형"
},
"certificates.key-type-description": {
"defaultMessage": "RSA는 호환성이 넓고, ECDSA는 더 빠르고 안전하지만 오래된 시스템에서 지원되지 않을 수 있습니다"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encrypt 사용"
},
"certificates.request.title": {
"defaultMessage": "새 인증서 요청"
},
"column.access": {
"defaultMessage": "접근 정책"
},
"column.authorization": {
"defaultMessage": "인증 사용자"
},
"column.authorizations": {
"defaultMessage": "인증 사용자"
},
"column.custom-locations": {
"defaultMessage": "사용자 지정 경로"
},
"column.destination": {
"defaultMessage": "전달 대상"
},
"column.details": {
"defaultMessage": "기본 설정"
},
"column.email": {
"defaultMessage": "이메일"
},
"column.event": {
"defaultMessage": "이벤트"
},
"column.expires": {
"defaultMessage": "만료일"
},
"column.http-code": {
"defaultMessage": "HTTP 코드"
},
"column.incoming-port": {
"defaultMessage": "수신 포트"
},
"column.name": {
"defaultMessage": "이름"
},
"column.protocol": {
"defaultMessage": "프로토콜"
},
"column.provider": {
"defaultMessage": "공급자"
},
"column.roles": {
"defaultMessage": "권한"
},
"column.rules": {
"defaultMessage": "IP 정책"
},
"column.satisfy": {
"defaultMessage": "조건 방식"
},
"column.satisfy-all": {
"defaultMessage": "모두 충족"
},
"column.satisfy-any": {
"defaultMessage": "하나라도 충족"
},
"column.scheme": {
"defaultMessage": "프로토콜"
},
"column.source": {
"defaultMessage": "도메인"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "상태"
},
"created-on": {
"defaultMessage": "생성일: {date}"
},
"dashboard": {
"defaultMessage": "대시보드"
},
"dead-host": {
"defaultMessage": "404 호스트"
},
"dead-hosts": {
"defaultMessage": "404 호스트"
},
"dead-hosts.count": {
"defaultMessage": "{count}개의 404 호스트"
},
"disabled": {
"defaultMessage": "비활성화"
},
"domain-names": {
"defaultMessage": "도메인 이름"
},
"domain-names.max": {
"defaultMessage": "최대 {count}개의 도메인 이름"
},
"domain-names.placeholder": {
"defaultMessage": "도메인을 입력해주세요."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "HTTP 방식으로는 와일드카드 인증서를 발급할 수 없습니다."
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "이 인증 기관(CA)은 와일드카드를 지원하지 않습니다."
},
"domains.force-ssl": {
"defaultMessage": "SSL 강제 적용"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS 활성화"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS 서브도메인 포함"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 지원"
},
"domains.use-dns": {
"defaultMessage": "DNS 챌린지 사용"
},
"email-address": {
"defaultMessage": "이메일 주소"
},
"empty-search": {
"defaultMessage": "검색 결과 없음"
},
"empty-subtitle": {
"defaultMessage": "하나 만들어 보는 건 어떨까요?"
},
"enabled": {
"defaultMessage": "활성화"
},
"error.access.at-least-one": {
"defaultMessage": "인증 또는 접근 규칙 중 하나는 반드시 필요합니다."
},
"error.access.duplicate-usernames": {
"defaultMessage": "인증 사용자 이름은 중복될 수 없습니다."
},
"error.invalid-auth": {
"defaultMessage": "이메일 또는 비밀번호가 잘못되었습니다."
},
"error.invalid-domain": {
"defaultMessage": "잘못된 도메인: {domain}"
},
"error.invalid-email": {
"defaultMessage": "잘못된 이메일 주소입니다."
},
"error.max-character-length": {
"defaultMessage": "최대 길이는 {max}자입니다."
},
"error.max-domains": {
"defaultMessage": "도메인이 너무 많습니다. 최대 {max}개까지 가능합니다."
},
"error.maximum": {
"defaultMessage": "최댓값은 {max}입니다."
},
"error.min-character-length": {
"defaultMessage": "최소 길이는 {min}자입니다."
},
"error.minimum": {
"defaultMessage": "최솟값은 {min}입니다."
},
"error.passwords-must-match": {
"defaultMessage": "비밀번호가 일치해야 합니다."
},
"error.required": {
"defaultMessage": "필수 항목입니다."
},
"expires.on": {
"defaultMessage": "만료일: {date}"
},
"footer.github-fork": {
"defaultMessage": "GitHub에서 포크하기"
},
"host.flags.block-exploits": {
"defaultMessage": "일반적인 공격 차단"
},
"host.flags.cache-assets": {
"defaultMessage": "정적 에셋 캐싱"
},
"host.flags.preserve-path": {
"defaultMessage": "요청 경로 유지"
},
"host.flags.protocols": {
"defaultMessage": "프로토콜"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "웹소켓 지원"
},
"host.forward-port": {
"defaultMessage": "전달할 포트"
},
"host.forward-scheme": {
"defaultMessage": "프로토콜"
},
"hosts": {
"defaultMessage": "호스트 목록"
},
"http-only": {
"defaultMessage": "HTTP 전용"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt (DNS 방식)"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt (HTTP 방식)"
},
"loading": {
"defaultMessage": "불러오는 중…"
},
"login.title": {
"defaultMessage": "로그인"
},
"nginx-config.label": {
"defaultMessage": "사용자 지정 Nginx 설정"
},
"nginx-config.placeholder": {
"defaultMessage": "# 위험을 감수하고 여기에 사용자 지정 Nginx 설정을 입력하세요!"
},
"no-permission-error": {
"defaultMessage": "이 내용을 볼 권한이 없습니다."
},
"notfound.action": {
"defaultMessage": "홈으로 이동"
},
"notfound.content": {
"defaultMessage": "죄송합니다. 찾으시는 페이지를 찾을 수 없습니다."
},
"notfound.title": {
"defaultMessage": "이런… 오류 페이지에 도착했습니다."
},
"notification.error": {
"defaultMessage": "오류"
},
"notification.object-deleted": {
"defaultMessage": "{object}이(가) 삭제되었습니다."
},
"notification.object-disabled": {
"defaultMessage": "{object}이(가) 비활성화되었습니다."
},
"notification.object-enabled": {
"defaultMessage": "{object}이(가) 활성화되었습니다."
},
"notification.object-renewed": {
"defaultMessage": "{object}이(가) 갱신되었습니다."
},
"notification.object-saved": {
"defaultMessage": "{object}이(가) 저장되었습니다."
},
"notification.success": {
"defaultMessage": "성공"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object} 추가"
},
"object.delete": {
"defaultMessage": "{object} 삭제"
},
"object.delete.content": {
"defaultMessage": "이 {object}을(를) 정말 삭제하시겠습니까?"
},
"object.edit": {
"defaultMessage": "{object} 편집"
},
"object.empty": {
"defaultMessage": "{objects}이(가) 없습니다."
},
"object.event.created": {
"defaultMessage": "{object}이(가) 생성됨"
},
"object.event.deleted": {
"defaultMessage": "{object}이(가) 삭제됨"
},
"object.event.disabled": {
"defaultMessage": "{object}이(가) 비활성화됨"
},
"object.event.enabled": {
"defaultMessage": "{object}이(가) 활성화됨"
},
"object.event.renewed": {
"defaultMessage": "{object}이(가) 갱신됨"
},
"object.event.updated": {
"defaultMessage": "{object}이(가) 업데이트됨"
},
"offline": {
"defaultMessage": "비활성화"
},
"online": {
"defaultMessage": "활성화"
},
"options": {
"defaultMessage": "옵션"
},
"password": {
"defaultMessage": "비밀번호"
},
"password.generate": {
"defaultMessage": "무작위 비밀번호 생성"
},
"password.hide": {
"defaultMessage": "비밀번호 숨기기"
},
"password.show": {
"defaultMessage": "비밀번호 표시"
},
"permissions.hidden": {
"defaultMessage": "숨김"
},
"permissions.manage": {
"defaultMessage": "관리"
},
"permissions.view": {
"defaultMessage": "보기 전용"
},
"permissions.visibility.all": {
"defaultMessage": "모든 항목"
},
"permissions.visibility.title": {
"defaultMessage": "항목 표시 설정"
},
"permissions.visibility.user": {
"defaultMessage": "내가 만든 항목만"
},
"proxy-host": {
"defaultMessage": "프록시 호스트"
},
"proxy-host.forward-host": {
"defaultMessage": "전달할 호스트명 / IP"
},
"proxy-hosts": {
"defaultMessage": "프록시 호스트"
},
"proxy-hosts.count": {
"defaultMessage": "{count}개의 프록시 호스트"
},
"public": {
"defaultMessage": "공개"
},
"redirection-host": {
"defaultMessage": "리다이렉션 호스트"
},
"redirection-host.forward-domain": {
"defaultMessage": "전달할 도메인"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP 코드"
},
"redirection-hosts": {
"defaultMessage": "리다이렉션 호스트"
},
"redirection-hosts.count": {
"defaultMessage": "{count}개의 리다이렉션 호스트"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiple Choices"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Moved permanently"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Moved temporarily"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 See other"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Temporary redirect"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Permanent redirect"
},
"role.admin": {
"defaultMessage": "관리자"
},
"role.standard-user": {
"defaultMessage": "일반 사용자"
},
"save": {
"defaultMessage": "저장"
},
"setting": {
"defaultMessage": "설정"
},
"settings": {
"defaultMessage": "설정"
},
"settings.default-site": {
"defaultMessage": "기본 사이트"
},
"settings.default-site.404": {
"defaultMessage": "404 페이지"
},
"settings.default-site.444": {
"defaultMessage": "응답 없음 (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "축하 페이지"
},
"settings.default-site.description": {
"defaultMessage": "알 수 없는 호스트로 요청이 들어왔을 때 표시할 내용"
},
"settings.default-site.html": {
"defaultMessage": "사용자 지정 HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "리다이렉트"
},
"setup.preamble": {
"defaultMessage": "관리자 계정을 만들어 시작하세요."
},
"setup.title": {
"defaultMessage": "환영합니다!"
},
"sign-in": {
"defaultMessage": "로그인"
},
"ssl-certificate": {
"defaultMessage": "SSL 인증서"
},
"stream": {
"defaultMessage": "호스트 스트림"
},
"stream.forward-host": {
"defaultMessage": "전달할 호스트"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com / 10.0.0.1 / IPv6"
},
"stream.incoming-port": {
"defaultMessage": "수신 포트"
},
"streams": {
"defaultMessage": "호스트 스트림"
},
"streams.count": {
"defaultMessage": "{count}개의 호스트 스트림"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "테스트"
},
"update-available": {
"defaultMessage": "업데이트 가능: {latestVersion}"
},
"user": {
"defaultMessage": "사용자"
},
"user.change-password": {
"defaultMessage": "비밀번호 변경"
},
"user.confirm-password": {
"defaultMessage": "비밀번호 확인"
},
"user.current-password": {
"defaultMessage": "현재 비밀번호"
},
"user.edit-profile": {
"defaultMessage": "프로필 편집"
},
"user.full-name": {
"defaultMessage": "전체 이름"
},
"user.login-as": {
"defaultMessage": "{name}으로 로그인"
},
"user.logout": {
"defaultMessage": "로그아웃"
},
"user.new-password": {
"defaultMessage": "새 비밀번호"
},
"user.nickname": {
"defaultMessage": "닉네임"
},
"user.set-password": {
"defaultMessage": "비밀번호 설정"
},
"user.set-permissions": {
"defaultMessage": "{name}의 권한 설정"
},
"user.switch-dark": {
"defaultMessage": "다크 모드로 전환"
},
"user.switch-light": {
"defaultMessage": "라이트 모드로 전환"
},
"username": {
"defaultMessage": "사용자 이름"
},
"users": {
"defaultMessage": "사용자"
}
}
================================================
FILE: frontend/src/locale/src/lang-list.json
================================================
{
"locale-en-US": {
"defaultMessage": "English"
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-et-EE": {
"defaultMessage": "Eesti"
},
"locale-ie-GA": {
"defaultMessage": "Gaeilge"
},
"locale-de-DE": {
"defaultMessage": "German"
},
"locale-pt-PT": {
"defaultMessage": "Português (Europeu)"
},
"locale-fr-FR": {
"defaultMessage": "Français"
},
"locale-id-ID": {
"defaultMessage": "Bahasa Indonesia"
},
"locale-ja-JP": {
"defaultMessage": "日本語"
},
"locale-ru-RU": {
"defaultMessage": "Русский"
},
"locale-sk-SK": {
"defaultMessage": "Slovenčina"
},
"locale-cs-CZ": {
"defaultMessage": "Čeština"
},
"locale-zh-CN": {
"defaultMessage": "中文"
},
"locale-pl-PL": {
"defaultMessage": "Polski"
},
"locale-it-IT": {
"defaultMessage": "Italiano"
},
"locale-vi-VN": {
"defaultMessage": "Tiếng Việt"
},
"locale-nl-NL": {
"defaultMessage": "Nederlands"
},
"locale-ko-KR": {
"defaultMessage": "한국어"
},
"locale-bg-BG": {
"defaultMessage": "Български"
},
"locale-tr-TR": {
"defaultMessage": "Türkçe"
},
"locale-hu-HU": {
"defaultMessage": "Magyar"
},
"locale-no-NO": {
"defaultMessage": "Norsk"
}
}
================================================
FILE: frontend/src/locale/src/nl.json
================================================
{
"access-list": {
"defaultMessage": "Toegangslijst"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regel} other {Regels}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Gebruiker} other {Gebruikers}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Als er minimaal 1 regel bestaat, wordt deze regel als laatste toegevoegd"
},
"access-list.help.rules-order": {
"defaultMessage": "Onthoud dat de regels van boven naar beneden worden toegevoegd."
},
"access-list.pass-auth": {
"defaultMessage": "Pass Auth to Upstream"
},
"access-list.public": {
"defaultMessage": "Publiekelijk toegankelijk"
},
"access-list.public.subtitle": {
"defaultMessage": "Geen basisautentificatie vereist"
},
"access-list.satisfy-any": {
"defaultMessage": "Voldoe aan elke"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Gebruiker} other {Gebruikers}}, {rules} {rules, plural, one {Regel} other {Regels}} - Aangemaakt: {date}"
},
"access-lists": {
"defaultMessage": "Toegangslijsten"
},
"action.add": {
"defaultMessage": "Toevoegen"
},
"action.add-location": {
"defaultMessage": "Locatie Toevoegen"
},
"action.close": {
"defaultMessage": "Sluiten"
},
"action.delete": {
"defaultMessage": "Verwijderen"
},
"action.disable": {
"defaultMessage": "Uitzetten"
},
"action.download": {
"defaultMessage": "Download"
},
"action.edit": {
"defaultMessage": "Bewerken"
},
"action.enable": {
"defaultMessage": "Aanzetten"
},
"action.permissions": {
"defaultMessage": "Rechten"
},
"action.renew": {
"defaultMessage": "Vernieuwen"
},
"action.view-details": {
"defaultMessage": "Bekijk Details"
},
"auditlogs": {
"defaultMessage": "Logboeken"
},
"cancel": {
"defaultMessage": "Annuleren"
},
"certificate": {
"defaultMessage": "Certificaat"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificaat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Certificaat Sleutel"
},
"certificate.custom-intermediate": {
"defaultMessage": "Intermediate Certificaat"
},
"certificate.in-use": {
"defaultMessage": "In Gebruik"
},
"certificate.none.subtitle": {
"defaultMessage": "Geen certificaat toegewezen"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Deze host gebruikt geen HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Geen"
},
"certificate.not-in-use": {
"defaultMessage": "Niet Gebruikt"
},
"certificate.renew": {
"defaultMessage": "Certificaat Vernieuwen"
},
"certificates": {
"defaultMessage": "Certificaten"
},
"certificates.custom": {
"defaultMessage": "Aangepast Certificaat"
},
"certificates.custom.warning": {
"defaultMessage": "Sleutels met een wachtzin zijn niet ondersteund."
},
"certificates.dns.credentials": {
"defaultMessage": "Credentials File Content"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Deze plugin vereist een configuratiebestand met een API token of andere gegevens van de provider."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Deze data zal worden opgeslagen als plaintext in de database en in een bestand!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Verwerkingstijd (seconden)"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Laat leeg om de standaardwaarde van de plugin te gebruiken. Aantal seconden om te wachten op DNS propagatie."
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.warning": {
"defaultMessage": "Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Jouw server is bereikbaar en certificaten kunnen worden aangemaakt."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Testresultaten"
},
"certificates.http.warning": {
"defaultMessage": "Deze domeinen moeten al worden geconfigureerd om naar deze installatie te wijzen."
},
"certificates.key-type": {
"defaultMessage": "Sleuteltype"
},
"certificates.key-type-description": {
"defaultMessage": "RSA is breed compatibel, ECDSA is sneller en veiliger maar wordt mogelijk niet ondersteund door oudere systemen"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "met Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Vraag een nieuwe Certificaat aan"
},
"column.access": {
"defaultMessage": "Toegang"
},
"column.authorization": {
"defaultMessage": "Authorizatie"
},
"column.authorizations": {
"defaultMessage": "Authorizaties"
},
"column.custom-locations": {
"defaultMessage": "Aangepaste Locaties"
},
"column.destination": {
"defaultMessage": "Doel"
},
"column.details": {
"defaultMessage": "Details"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Gebeurtenis"
},
"column.expires": {
"defaultMessage": "Verloopt"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Inkomende Poort"
},
"column.name": {
"defaultMessage": "Naam"
},
"column.protocol": {
"defaultMessage": "Protocol"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Rollen"
},
"column.rules": {
"defaultMessage": "Regels"
},
"column.satisfy": {
"defaultMessage": "Vervul"
},
"column.satisfy-all": {
"defaultMessage": "Alle"
},
"column.satisfy-any": {
"defaultMessage": "Elke"
},
"column.scheme": {
"defaultMessage": "Schema"
},
"column.source": {
"defaultMessage": "Bron"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Aangemaakt: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Hosts"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
},
"disabled": {
"defaultMessage": "Uitgezet"
},
"domain-names": {
"defaultMessage": "Domeinnamen"
},
"domain-names.max": {
"defaultMessage": "{count} domeinnamen toegestaan"
},
"domain-names.placeholder": {
"defaultMessage": "Voeg een domeinnaam toe..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards zijn niet toegestaan voor dit type"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards zijn niet ondersteund voor deze CA"
},
"domains.force-ssl": {
"defaultMessage": "Forceer SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Aangezet"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Subdomein"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Ondersteuning"
},
"domains.use-dns": {
"defaultMessage": "Gebruik DNS Challenge"
},
"email-address": {
"defaultMessage": "E-mailadres"
},
"empty-search": {
"defaultMessage": "Geen resultaten gevonden"
},
"empty-subtitle": {
"defaultMessage": "Waarom niet een maken?"
},
"enabled": {
"defaultMessage": "Aangezet"
},
"error.access.at-least-one": {
"defaultMessage": "Minimaal één authorizatie- of één toegangsregel is vereist"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Gebruikersnamen moeten uniek zijn"
},
"error.invalid-auth": {
"defaultMessage": "Ongeldige email of wachtwoord"
},
"error.invalid-domain": {
"defaultMessage": "Ongeldige domeinnaam: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Ongeldig e-mailadres"
},
"error.max-character-length": {
"defaultMessage": "Maximale lengte is {max} karakter{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Te veel domeinnamen, max is {max}"
},
"error.maximum": {
"defaultMessage": "Maximale is {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimale lengte is {min} karakter{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Minimale is {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Wachtwoorden moeten overeenkomen"
},
"error.required": {
"defaultMessage": "Dit is verplicht"
},
"expires.on": {
"defaultMessage": "Verloopt: {date}"
},
"footer.github-fork": {
"defaultMessage": "Maak een Fork op Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokkeer Veelvoorkomende Kwetsbaarheden"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Assets"
},
"host.flags.preserve-path": {
"defaultMessage": "Pad Behouden"
},
"host.flags.protocols": {
"defaultMessage": "Protocollen"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Ondersteuning"
},
"host.forward-port": {
"defaultMessage": "Poort Doorsturen"
},
"host.forward-scheme": {
"defaultMessage": "Schema"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "Alleen HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Laden…"
},
"login.title": {
"defaultMessage": "Inloggen"
},
"nginx-config.label": {
"defaultMessage": "Aangepaste Nginx Configuratie"
},
"nginx-config.placeholder": {
"defaultMessage": "# Voeg jouw aangepaste Nginx configuratie hier op eigen risico toe!"
},
"no-permission-error": {
"defaultMessage": "Jij hebt geen toegang om dit te bekijken."
},
"notfound.action": {
"defaultMessage": "Thuis"
},
"notfound.content": {
"defaultMessage": "De pagina waar je naar op zoek bent kan niet worden gevonden"
},
"notfound.title": {
"defaultMessage": "Oeps… Je hebt een foutpagina gevonden"
},
"notification.error": {
"defaultMessage": "Fout"
},
"notification.object-deleted": {
"defaultMessage": "{object} is verwijderd"
},
"notification.object-disabled": {
"defaultMessage": "{object} is uitgezet"
},
"notification.object-enabled": {
"defaultMessage": "{object} is aangezet"
},
"notification.object-renewed": {
"defaultMessage": "{object} is vernieuwd"
},
"notification.object-saved": {
"defaultMessage": "{object} is opgeslagen"
},
"notification.success": {
"defaultMessage": "Succes"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Voeg {object} toe"
},
"object.delete": {
"defaultMessage": "Verwijder {object}"
},
"object.delete.content": {
"defaultMessage": "Weet je zeker dat je {object} wilt verwijderen?"
},
"object.edit": {
"defaultMessage": "Bewerk {object}"
},
"object.empty": {
"defaultMessage": "Er zijn geen {objects}"
},
"object.event.created": {
"defaultMessage": "{object} is aangemaakt"
},
"object.event.deleted": {
"defaultMessage": "{object} is verwijderd"
},
"object.event.disabled": {
"defaultMessage": "{object} is uitgezet"
},
"object.event.enabled": {
"defaultMessage": "{object} is aangezet"
},
"object.event.renewed": {
"defaultMessage": "{object} is vernieuwd"
},
"object.event.updated": {
"defaultMessage": "{object} is bijgewerkt"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opties"
},
"password": {
"defaultMessage": "Wachtwoord"
},
"password.generate": {
"defaultMessage": "Willekeurig wachtwoord genereren"
},
"password.hide": {
"defaultMessage": "Wachtwoord Verbergen"
},
"password.show": {
"defaultMessage": "Toon Wachtwoord"
},
"permissions.hidden": {
"defaultMessage": "Verborgen"
},
"permissions.manage": {
"defaultMessage": "Beheer"
},
"permissions.view": {
"defaultMessage": "Alleen Bekijken"
},
"permissions.visibility.all": {
"defaultMessage": "Alle Items"
},
"permissions.visibility.title": {
"defaultMessage": "Item Zichtbaarheid"
},
"permissions.visibility.user": {
"defaultMessage": "Alleen Aangemaakte Items"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Hostname / IP Doorsturen"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Openbaar"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Doorgestuurd Domein"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
},
"role.admin": {
"defaultMessage": "Beheerder"
},
"role.standard-user": {
"defaultMessage": "Standaard Gebruiker"
},
"save": {
"defaultMessage": "Opslaan"
},
"setting": {
"defaultMessage": "Instelling"
},
"settings": {
"defaultMessage": "Instellingen"
},
"settings.default-site": {
"defaultMessage": "Standaard Site"
},
"settings.default-site.404": {
"defaultMessage": "404 Pagina"
},
"settings.default-site.444": {
"defaultMessage": "Geen Antwoord (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Felicitatiepagina"
},
"settings.default-site.description": {
"defaultMessage": "Wat te tonen als Nginx een onbekende Host ontvangt"
},
"settings.default-site.html": {
"defaultMessage": "Aangepaste HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Omleiding"
},
"setup.preamble": {
"defaultMessage": "Begin met het aanmaken van je beheerder account."
},
"setup.title": {
"defaultMessage": "Welkom!"
},
"sign-in": {
"defaultMessage": "Inloggen"
},
"ssl-certificate": {
"defaultMessage": "SSL Certificaten"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Doorgestuurde Host"
},
"stream.incoming-port": {
"defaultMessage": "Inkomende Poort"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Update Beschikbaar: {latestVersion}"
},
"user": {
"defaultMessage": "Gebruiker"
},
"user.change-password": {
"defaultMessage": "Verander Wachtwoord"
},
"user.confirm-password": {
"defaultMessage": "Bevestig Wachtwoord"
},
"user.current-password": {
"defaultMessage": "Huidig Wachtwoord"
},
"user.edit-profile": {
"defaultMessage": "Profiel Bewerken"
},
"user.full-name": {
"defaultMessage": "Volledige Naam"
},
"user.login-as": {
"defaultMessage": "Inloggen als {name}"
},
"user.logout": {
"defaultMessage": "Uitloggen"
},
"user.new-password": {
"defaultMessage": "Nieuw Wachtwoord"
},
"user.nickname": {
"defaultMessage": "Bijnaam"
},
"user.set-password": {
"defaultMessage": "Zet Wachtwoord"
},
"user.set-permissions": {
"defaultMessage": "Zet machtigingen voor {name}"
},
"user.switch-dark": {
"defaultMessage": "Verander naar donkere modus"
},
"user.switch-light": {
"defaultMessage": "Verander naar lichte modus"
},
"username": {
"defaultMessage": "Gebruikersnaam"
},
"users": {
"defaultMessage": "Gebruikers"
}
}
================================================
FILE: frontend/src/locale/src/no.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Gjenstående backup-koder: {count}"
},
"2fa.backup-warning": {
"defaultMessage": "Lagre disse backup-kodene på et sikkert sted. Hver kode kan kun brukes én gang."
},
"2fa.disable": {
"defaultMessage": "Deaktiver tofaktorautentisering"
},
"2fa.disable-confirm": {
"defaultMessage": "Deaktiver 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Å deaktivere tofaktorautentisering vil gjøre kontoen din mindre sikker."
},
"2fa.disabled": {
"defaultMessage": "Deaktivert"
},
"2fa.done": {
"defaultMessage": "Jeg har lagret backup-kodene mine"
},
"2fa.enable": {
"defaultMessage": "Aktiver tofaktorautentisering"
},
"2fa.enabled": {
"defaultMessage": "Aktivert"
},
"2fa.enter-code": {
"defaultMessage": "Angi verifiseringskode"
},
"2fa.enter-code-disable": {
"defaultMessage": "Angi verifiseringskode for å deaktivere"
},
"2fa.regenerate": {
"defaultMessage": "Regenerer"
},
"2fa.regenerate-backup": {
"defaultMessage": "Generer nye backup-koder"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Angi en verifiseringskode for å generere nye backup-koder. Dine gamle koder vil bli ugyldige."
},
"2fa.secret-key": {
"defaultMessage": "Hemmelig nøkkel"
},
"2fa.setup-instructions": {
"defaultMessage": "Skann denne QR-koden med autentiseringsappen din, eller skriv inn nøkkelen manuelt."
},
"2fa.status": {
"defaultMessage": "Status"
},
"2fa.title": {
"defaultMessage": "Tofaktorautentisering"
},
"2fa.verify-enable": {
"defaultMessage": "Verifiser og aktiver"
},
"access-list": {
"defaultMessage": "Tilgangsliste"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {regel} other {regler}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {bruker} other {brukere}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Når minst én regel finnes, legges denne \"deny all\"-regelen til sist"
},
"access-list.help.rules-order": {
"defaultMessage": "Merk at tillat- og nekt-direktivene brukes i den rekkefølgen de er definert."
},
"access-list.pass-auth": {
"defaultMessage": "Send autentisering til upstream"
},
"access-list.public": {
"defaultMessage": "Offentlig tilgjengelig"
},
"access-list.public.subtitle": {
"defaultMessage": "Ingen grunnleggende autentisering kreves"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 eller 192.168.1.0/24 eller 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Oppfyll en av kravene"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {bruker} other {brukere}}, {rules} {rules, plural, one {regel} other {regler}} - Opprettet: {date}"
},
"access-lists": {
"defaultMessage": "Tilgangslister"
},
"action.add": {
"defaultMessage": "Legg til"
},
"action.add-location": {
"defaultMessage": "Legg til plassering"
},
"action.allow": {
"defaultMessage": "Tillat"
},
"action.close": {
"defaultMessage": "Lukk"
},
"action.delete": {
"defaultMessage": "Slett"
},
"action.deny": {
"defaultMessage": "Nekt"
},
"action.disable": {
"defaultMessage": "Deaktiver"
},
"action.download": {
"defaultMessage": "Last ned"
},
"action.edit": {
"defaultMessage": "Rediger"
},
"action.enable": {
"defaultMessage": "Aktiver"
},
"action.permissions": {
"defaultMessage": "Tillatelser"
},
"action.renew": {
"defaultMessage": "Forny"
},
"action.view-details": {
"defaultMessage": "Vis detaljer"
},
"auditlogs": {
"defaultMessage": "Revisjonslogger"
},
"auto": {
"defaultMessage": "Auto"
},
"cancel": {
"defaultMessage": "Avbryt"
},
"certificate": {
"defaultMessage": "Sertifikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Egendefinert Sertifikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Egendefinert Sertifikat nøkkel"
},
"certificate.custom-intermediate": {
"defaultMessage": "Egendefinert Intermediate Sertifikat"
},
"certificate.in-use": {
"defaultMessage": "I bruk"
},
"certificate.none.subtitle": {
"defaultMessage": "Ingen sertifikat tildelt"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Denne verten vil ikke bruke HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Ingen"
},
"certificate.not-in-use": {
"defaultMessage": "Ikke i bruk"
},
"certificate.renew": {
"defaultMessage": "Forny sertifikat"
},
"certificates": {
"defaultMessage": "Sertifikater"
},
"certificates.custom": {
"defaultMessage": "Egendefinert Sertifikat"
},
"certificates.custom.warning": {
"defaultMessage": "Nøkkelfiler beskyttet med passordfrase støttes ikke."
},
"certificates.dns.credentials": {
"defaultMessage": "Innhold i legitimasjonsfil"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Denne pluginen krever en konfigurasjonsfil som inneholder en API-token eller andre legitimasjoner for leverandøren din"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Disse dataene vil bli lagret som ren tekst i databasen og i en fil!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propageringsekunder"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "La stå tomt for å bruke pluginens standardverdi. Antall sekunder å vente på DNS-propagasjon."
},
"certificates.dns.provider": {
"defaultMessage": "DNS-leverandør"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Velg en leverandør..."
},
"certificates.dns.warning": {
"defaultMessage": "Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Serveren din er tilgjengelig, og det bør være mulig å opprette sertifikater."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Testresultater"
},
"certificates.http.warning": {
"defaultMessage": "Disse domenene må allerede være konfigurert til å peke til denne installasjonen."
},
"certificates.key-type": {
"defaultMessage": "Nøkkeltype"
},
"certificates.key-type-description": {
"defaultMessage": "RSA er bredt kompatibel, ECDSA er raskere og mer sikker, men støttes kanskje ikke av eldre systemer"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "med Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Be om et nytt sertifikat"
},
"column.access": {
"defaultMessage": "Tilgang"
},
"column.authorization": {
"defaultMessage": "Autorisasjon"
},
"column.authorizations": {
"defaultMessage": "Autorisasjoner"
},
"column.custom-locations": {
"defaultMessage": "Egendefinerte plasseringer"
},
"column.destination": {
"defaultMessage": "Destinasjon"
},
"column.details": {
"defaultMessage": "Detaljer"
},
"column.email": {
"defaultMessage": "E-post"
},
"column.event": {
"defaultMessage": "Hendelse"
},
"column.expires": {
"defaultMessage": "Utløper"
},
"column.http-code": {
"defaultMessage": "HTTP-kode"
},
"column.incoming-port": {
"defaultMessage": "Innkommende port"
},
"column.name": {
"defaultMessage": "Navn"
},
"column.protocol": {
"defaultMessage": "Protokoll"
},
"column.provider": {
"defaultMessage": "Leverandør"
},
"column.roles": {
"defaultMessage": "Roller"
},
"column.rules": {
"defaultMessage": "Regler"
},
"column.satisfy": {
"defaultMessage": "Oppfylle"
},
"column.satisfy-all": {
"defaultMessage": "Alle"
},
"column.satisfy-any": {
"defaultMessage": "Noen"
},
"column.scheme": {
"defaultMessage": "Skjema"
},
"column.source": {
"defaultMessage": "Kilde"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Opprettet: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Tjener ikke funnet"
},
"dead-hosts": {
"defaultMessage": "404 Tjenere ikke funnet"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Tjener} other {404 Tjenere}}"
},
"disabled": {
"defaultMessage": "Deaktivert"
},
"domain-names": {
"defaultMessage": "Domener"
},
"domain-names.max": {
"defaultMessage": "{count} domener maksimum"
},
"domain-names.placeholder": {
"defaultMessage": "Begynn å skrive for å legge til domene..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards er ikke tillatt for denne typen"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards støttes ikke for denne CA-en"
},
"domains.advanced": {
"defaultMessage": "Avansert"
},
"domains.force-ssl": {
"defaultMessage": "Tving SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Aktivert"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Underdomener"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Støtte"
},
"domains.trust-forwarded-proto": {
"defaultMessage": "Stol på Upstream Forwarded Proto Headers"
},
"domains.use-dns": {
"defaultMessage": "Bruk DNS Utfordring"
},
"email-address": {
"defaultMessage": "E-postadresse"
},
"empty-search": {
"defaultMessage": "Ingen resultater funnet"
},
"empty-subtitle": {
"defaultMessage": "Hvorfor ikke opprette en?"
},
"enabled": {
"defaultMessage": "Aktivert"
},
"error.access.at-least-one": {
"defaultMessage": "Enten en autorisasjon eller en tilgangsregel er påkrevd"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Autorisasjonsbrukernavn må være unike"
},
"error.invalid-auth": {
"defaultMessage": "Ugyldig e-post eller passord"
},
"error.invalid-domain": {
"defaultMessage": "Ugyldig domene: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Ugyldig e-postadresse"
},
"error.max-character-length": {
"defaultMessage": "Maksimal lengde er {max} tegn"
},
"error.max-domains": {
"defaultMessage": "For mange domener, maks er {max}"
},
"error.maximum": {
"defaultMessage": "Maksimum er {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimum lengde er {min} tegn"
},
"error.minimum": {
"defaultMessage": "Minimum er {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Passordene må være like"
},
"error.required": {
"defaultMessage": "Dette er påkrevd"
},
"expires.on": {
"defaultMessage": "Utløper: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork meg på Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokker vanlige utnyttelser"
},
"host.flags.cache-assets": {
"defaultMessage": "Mellomlagre ressurser"
},
"host.flags.preserve-path": {
"defaultMessage": "Behold sti"
},
"host.flags.protocols": {
"defaultMessage": "Protokoller"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets-støtte"
},
"host.forward-port": {
"defaultMessage": "Viderekoble Port"
},
"host.forward-scheme": {
"defaultMessage": "Skjema"
},
"hosts": {
"defaultMessage": "Vertsnavn"
},
"http-only": {
"defaultMessage": "Kun HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Laster…"
},
"login.2fa-code": {
"defaultMessage": "Verifikasjonskode"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Skriv inn kode"
},
"login.2fa-description": {
"defaultMessage": "Skriv inn koden fra autentiseringsappen din"
},
"login.2fa-title": {
"defaultMessage": "To-faktorautentisering"
},
"login.2fa-verify": {
"defaultMessage": "Verifiser"
},
"login.title": {
"defaultMessage": "Logg på kontoen din"
},
"nginx-config.label": {
"defaultMessage": "Egendefinert Nginx-konfigurasjon"
},
"nginx-config.placeholder": {
"defaultMessage": "# Skriv inn din egendefinerte Nginx-konfigurasjon her på egen risiko!"
},
"no-permission-error": {
"defaultMessage": "Du har ikke tilgang til å se dette."
},
"notfound.action": {
"defaultMessage": "Ta meg hjem"
},
"notfound.content": {
"defaultMessage": "Beklager, siden du leter etter ble ikke funnet"
},
"notfound.title": {
"defaultMessage": "Oops… Du har nettopp funnet en feilsiden"
},
"notification.error": {
"defaultMessage": "Feil"
},
"notification.object-deleted": {
"defaultMessage": "{object} har blitt slettet"
},
"notification.object-disabled": {
"defaultMessage": "{object} har blitt deaktivert"
},
"notification.object-enabled": {
"defaultMessage": "{object} har blitt aktivert"
},
"notification.object-renewed": {
"defaultMessage": "{object} har blitt fornyet"
},
"notification.object-saved": {
"defaultMessage": "{object} har blitt lagret"
},
"notification.success": {
"defaultMessage": "Suksess"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Legg til {object}"
},
"object.delete": {
"defaultMessage": "Slett {object}"
},
"object.delete.content": {
"defaultMessage": "Er du sikker på at du vil slette dette {object}?"
},
"object.edit": {
"defaultMessage": "Rediger {object}"
},
"object.empty": {
"defaultMessage": "Det finnes ingen {objects}"
},
"object.event.created": {
"defaultMessage": "Opprettet {object}"
},
"object.event.deleted": {
"defaultMessage": "Slettet {object}"
},
"object.event.disabled": {
"defaultMessage": "Deaktivert {object}"
},
"object.event.enabled": {
"defaultMessage": "Aktivert {object}"
},
"object.event.renewed": {
"defaultMessage": "Fornyet {object}"
},
"object.event.updated": {
"defaultMessage": "Oppdatert {object}"
},
"offline": {
"defaultMessage": "Utilgjengelig"
},
"online": {
"defaultMessage": "Tilgjengelig"
},
"options": {
"defaultMessage": "Alternativer"
},
"password": {
"defaultMessage": "Passord"
},
"password.generate": {
"defaultMessage": "Generer tilfeldig passord"
},
"password.hide": {
"defaultMessage": "Skjul passord"
},
"password.show": {
"defaultMessage": "Vis passord"
},
"permissions.hidden": {
"defaultMessage": "Skjult"
},
"permissions.manage": {
"defaultMessage": "Administrer"
},
"permissions.view": {
"defaultMessage": "Kun visning"
},
"permissions.visibility.all": {
"defaultMessage": "Alle elementer"
},
"permissions.visibility.title": {
"defaultMessage": "Element Synlighet"
},
"permissions.visibility.user": {
"defaultMessage": "Kun opprettede elementer"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy-verter"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy-vert} other {Proxy-verter}}"
},
"public": {
"defaultMessage": "Offentlig"
},
"redirection-host": {
"defaultMessage": "Omdirigeringsvert"
},
"redirection-host.forward-domain": {
"defaultMessage": "Viderekoble domene"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP-kode"
},
"redirection-hosts": {
"defaultMessage": "Omdirigeringsverter"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Omdirigeringsvert} other {Omdirigeringsverter}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Multiple Choices"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Flyttet permanent"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Flyttet midlertidig"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Se andre"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Midlertidig omdirigering"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Permanent omdirigering"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standardbruker"
},
"save": {
"defaultMessage": "Lagre"
},
"setting": {
"defaultMessage": "Innstilling"
},
"settings": {
"defaultMessage": "Innstillinger"
},
"settings.default-site": {
"defaultMessage": "Standardnettsted"
},
"settings.default-site.404": {
"defaultMessage": "404-side"
},
"settings.default-site.444": {
"defaultMessage": "Ingen respons (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Gratulerer-side"
},
"settings.default-site.description": {
"defaultMessage": "Hva som skal vises når Nginx treffes med en ukjent vert"
},
"settings.default-site.html": {
"defaultMessage": "Egendefinert HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Omdiriger"
},
"setup.preamble": {
"defaultMessage": "Kom i gang ved å opprette din administratorkonto."
},
"setup.title": {
"defaultMessage": "Velkommen!"
},
"sign-in": {
"defaultMessage": "Logg inn"
},
"ssl-certificate": {
"defaultMessage": "SSL-sertifikat"
},
"stream": {
"defaultMessage": "Strøm"
},
"stream.forward-host": {
"defaultMessage": "Viderekoble vert"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com eller 10.0.0.1 eller 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Innkommende port"
},
"streams": {
"defaultMessage": "Strømmer"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Strøm} other {Strømmer}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Oppdatering tilgjengelig: {latestVersion}"
},
"user": {
"defaultMessage": "Bruker"
},
"user.change-password": {
"defaultMessage": "Endre passord"
},
"user.confirm-password": {
"defaultMessage": "Bekreft passord"
},
"user.current-password": {
"defaultMessage": "Nåværende passord"
},
"user.edit-profile": {
"defaultMessage": "Rediger profil"
},
"user.full-name": {
"defaultMessage": "Fullt navn"
},
"user.login-as": {
"defaultMessage": "Logg inn som {name}"
},
"user.logout": {
"defaultMessage": "Logg ut"
},
"user.new-password": {
"defaultMessage": "Nytt passord"
},
"user.nickname": {
"defaultMessage": "Kallenavn"
},
"user.set-password": {
"defaultMessage": "Angi passord"
},
"user.set-permissions": {
"defaultMessage": "Angi tillatelser for {name}"
},
"user.switch-dark": {
"defaultMessage": "Bytt til mørk modus"
},
"user.switch-light": {
"defaultMessage": "Bytt til lys modus"
},
"user.two-factor": {
"defaultMessage": "To-faktor autentisering"
},
"username": {
"defaultMessage": "Brukernavn"
},
"users": {
"defaultMessage": "Brukere"
}
}
================================================
FILE: frontend/src/locale/src/pl.json
================================================
{
"access-list": {
"defaultMessage": "wpis listy dostępu"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Reguła} few {Reguły} other {Reguł}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkownicy} other {Użytkowników}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Gdy istnieje co najmniej 1 reguła, ta reguła blokująca wszystko zostanie dodana na końcu"
},
"access-list.help.rules-order": {
"defaultMessage": "Należy pamiętać, że dyrektywy zezwolenia i odmowy będą stosowane w kolejności, w jakiej zostały zdefiniowane."
},
"access-list.pass-auth": {
"defaultMessage": "Przekaż uwierzytelnienie do serwera docelowego"
},
"access-list.public": {
"defaultMessage": "Publicznie dostępne"
},
"access-list.public.subtitle": {
"defaultMessage": "Nie wymaga uwierzytelnienia podstawowego"
},
"access-list.satisfy-any": {
"defaultMessage": "Spełnij dowolny warunek"
},
"access-list.subtitle": {
"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}"
},
"access-lists": {
"defaultMessage": "Listy dostępu"
},
"action.add": {
"defaultMessage": "Dodaj"
},
"action.add-location": {
"defaultMessage": "Dodaj lokalizację"
},
"action.allow": {
"defaultMessage": "Zezwól"
},
"action.close": {
"defaultMessage": "Zamknij"
},
"action.delete": {
"defaultMessage": "Usuń"
},
"action.deny": {
"defaultMessage": "Odrzuć"
},
"action.disable": {
"defaultMessage": "Wyłącz"
},
"action.download": {
"defaultMessage": "Pobierz"
},
"action.edit": {
"defaultMessage": "Edytuj"
},
"action.enable": {
"defaultMessage": "Włącz"
},
"action.permissions": {
"defaultMessage": "Uprawnienia"
},
"action.renew": {
"defaultMessage": "Odnów"
},
"action.view-details": {
"defaultMessage": "Pokaż szczegóły"
},
"auditlogs": {
"defaultMessage": "Logi"
},
"cancel": {
"defaultMessage": "Anuluj"
},
"certificate": {
"defaultMessage": "certyfikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Certyfikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Klucz certyfikatu"
},
"certificate.custom-intermediate": {
"defaultMessage": "Certyfikat pośredni"
},
"certificate.in-use": {
"defaultMessage": "W użyciu"
},
"certificate.none.subtitle": {
"defaultMessage": "Nie przypisano certyfikatu"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Ten host nie będzie używał HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Brak"
},
"certificate.not-in-use": {
"defaultMessage": "Nie używany"
},
"certificate.renew": {
"defaultMessage": "Odnów certyfikat"
},
"certificates": {
"defaultMessage": "Certyfikaty"
},
"certificates.custom": {
"defaultMessage": "Własny certyfikat"
},
"certificates.custom.warning": {
"defaultMessage": "Pliki kluczy chronione hasłem nie są obsługiwane."
},
"certificates.dns.credentials": {
"defaultMessage": "Zawartość pliku z poświadczeniami"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Ta wtyczka wymaga pliku konfiguracyjnego zawierającego token API lub inne poświadczenia dla twojego dostawcy"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Te dane zostaną zapisane jako zwykły tekst w bazie danych i pliku!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Sekundy propagacji"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Pozostaw puste, aby użyć domyślnej wartości wtyczki. Liczba sekund oczekiwania na propagację DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Dostawca DNS"
},
"certificates.dns.warning": {
"defaultMessage": "Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Twój serwer jest dostępny i tworzenie certyfikatów powinno być możliwe."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Wyniki testu"
},
"certificates.http.warning": {
"defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer"
},
"certificates.key-type": {
"defaultMessage": "Typ klucza"
},
"certificates.key-type-description": {
"defaultMessage": "RSA jest szeroko kompatybilny, ECDSA jest szybszy i bezpieczniejszy, ale może nie być obsługiwany przez starsze systemy"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "z Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Wygeneruj nowy certyfikat"
},
"column.access": {
"defaultMessage": "Dostęp"
},
"column.authorization": {
"defaultMessage": "Autoryzacja"
},
"column.authorizations": {
"defaultMessage": "Autoryzacje"
},
"column.custom-locations": {
"defaultMessage": "Własne ustawienia lokalizacji"
},
"column.destination": {
"defaultMessage": "Cel"
},
"column.details": {
"defaultMessage": "Szczegóły"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Zdarzenie"
},
"column.expires": {
"defaultMessage": "Wygasa"
},
"column.http-code": {
"defaultMessage": "Kod HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port przychodzący"
},
"column.name": {
"defaultMessage": "Nazwa"
},
"column.protocol": {
"defaultMessage": "Protokół"
},
"column.provider": {
"defaultMessage": "Dostawca"
},
"column.roles": {
"defaultMessage": "Rola"
},
"column.rules": {
"defaultMessage": "Reguły"
},
"column.satisfy": {
"defaultMessage": "Spełnij"
},
"column.satisfy-all": {
"defaultMessage": "Wszystkie"
},
"column.satisfy-any": {
"defaultMessage": "Dowolny"
},
"column.scheme": {
"defaultMessage": "Schemat"
},
"column.source": {
"defaultMessage": "Źródło"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Utworzono: {date}"
},
"dashboard": {
"defaultMessage": "Panel"
},
"dead-host": {
"defaultMessage": "host 404"
},
"dead-hosts": {
"defaultMessage": "404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {host 404} few {hosty 404} other {hostów 404}}"
},
"disabled": {
"defaultMessage": "Wyłączone"
},
"domain-names": {
"defaultMessage": "Nazwy domen"
},
"domain-names.max": {
"defaultMessage": "Maksymalnie {count} nazw domen"
},
"domain-names.placeholder": {
"defaultMessage": "Zacznij pisać, aby dodać domenę..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Symbole wieloznaczne nie są dozwolone dla tego typu"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Symbole wieloznaczne nie są obsługiwane dla tego CA"
},
"domains.force-ssl": {
"defaultMessage": "Wymuś SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Włącz HSTS "
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS dla subdomen"
},
"domains.http2-support": {
"defaultMessage": "Obsługa HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Użyj wyzwania DNS"
},
"email-address": {
"defaultMessage": "Adres email"
},
"empty-search": {
"defaultMessage": "Nie znaleziono wyników"
},
"empty-subtitle": {
"defaultMessage": "Może utworzysz nowy?"
},
"enabled": {
"defaultMessage": "Włączone"
},
"error.access.at-least-one": {
"defaultMessage": "Wymagana jest co najmniej jedna autoryzacja lub jedna reguła dostępu"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Nazwy użytkowników autoryzacji muszą być unikalne"
},
"error.invalid-auth": {
"defaultMessage": "Nieprawidłowy email lub hasło"
},
"error.invalid-domain": {
"defaultMessage": "Nieprawidłowa domena: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Nieprawidłowy adres email"
},
"error.max-character-length": {
"defaultMessage": "Maksymalna długość to {max} {max, plural, one {znak} few {znaki} other {znaków}}"
},
"error.max-domains": {
"defaultMessage": "Zbyt wiele domen, maksimum to {max}"
},
"error.maximum": {
"defaultMessage": "Maksimum to {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimalna długość to {min} {min, plural, one {znak} few {znaki} other {znaków}}"
},
"error.minimum": {
"defaultMessage": "Minimum to {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Hasła muszą się zgadzać"
},
"error.required": {
"defaultMessage": "To pole jest wymagane"
},
"expires.on": {
"defaultMessage": "Wygasa: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forkuj mnie na Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokuj typowe exploity"
},
"host.flags.cache-assets": {
"defaultMessage": "Buforuj zasoby statyczne (ang. cache)"
},
"host.flags.preserve-path": {
"defaultMessage": "Zachowaj ścieżkę"
},
"host.flags.protocols": {
"defaultMessage": "Protokoły"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Obsługa WebSockets"
},
"host.forward-port": {
"defaultMessage": "Port docelowy"
},
"host.forward-scheme": {
"defaultMessage": "Schemat"
},
"hosts": {
"defaultMessage": "Hosty"
},
"http-only": {
"defaultMessage": "Tylko HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt przez DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt przez HTTP"
},
"loading": {
"defaultMessage": "Ładowanie…"
},
"login.title": {
"defaultMessage": "Zaloguj się na swoje konto"
},
"nginx-config.label": {
"defaultMessage": "Własna konfiguracja Nginx"
},
"nginx-config.placeholder": {
"defaultMessage": "# Wprowadź tutaj własną konfigurację Nginx na własną odpowiedzialność!"
},
"no-permission-error": {
"defaultMessage": "Nie masz uprawnień do wyświetlenia tego."
},
"notfound.action": {
"defaultMessage": "Zabierz mnie do strony głównej"
},
"notfound.content": {
"defaultMessage": "Przepraszamy, ale strona, której szukasz, nie została znaleziona"
},
"notfound.title": {
"defaultMessage": "Ups… Właśnie znalazłeś stronę błędu"
},
"notification.error": {
"defaultMessage": "Błąd"
},
"notification.object-deleted": {
"defaultMessage": "{object} został usunięty"
},
"notification.object-disabled": {
"defaultMessage": "{object} został wyłączony"
},
"notification.object-enabled": {
"defaultMessage": "{object} został włączony"
},
"notification.object-renewed": {
"defaultMessage": "{object} został odnowiony"
},
"notification.object-saved": {
"defaultMessage": "{object} został zapisany"
},
"notification.success": {
"defaultMessage": "Sukces"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Nowy {object}"
},
"object.delete": {
"defaultMessage": "Usuń {object}"
},
"object.delete.content": {
"defaultMessage": "Czy na pewno chcesz usunąć {object}?"
},
"object.edit": {
"defaultMessage": "Edytuj {object}"
},
"object.empty": {
"defaultMessage": "Brak {objects}"
},
"object.event.created": {
"defaultMessage": "Utworzono {object}"
},
"object.event.deleted": {
"defaultMessage": "Usunięto {object}"
},
"object.event.disabled": {
"defaultMessage": "Wyłączono {object}"
},
"object.event.enabled": {
"defaultMessage": "Włączono {object}"
},
"object.event.renewed": {
"defaultMessage": "Odnowiono {object}"
},
"object.event.updated": {
"defaultMessage": "Zaktualizowano {object}"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opcje"
},
"password": {
"defaultMessage": "Hasło"
},
"password.generate": {
"defaultMessage": "Wygeneruj losowe hasło"
},
"password.hide": {
"defaultMessage": "Ukryj hasło"
},
"password.show": {
"defaultMessage": "Pokaż hasło"
},
"permissions.hidden": {
"defaultMessage": "Ukryte"
},
"permissions.manage": {
"defaultMessage": "Zarządzaj"
},
"permissions.view": {
"defaultMessage": "Tylko podgląd"
},
"permissions.visibility.all": {
"defaultMessage": "Wszystkie elementy"
},
"permissions.visibility.title": {
"defaultMessage": "Widoczność elementów"
},
"permissions.visibility.user": {
"defaultMessage": "Tylko utworzone elementy"
},
"proxy-host": {
"defaultMessage": "host proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Przekieruj na hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {host proxy} few {hosty proxy} many {hostów proxy} other {hostów proxy}}"
},
"public": {
"defaultMessage": "Publiczne"
},
"redirection-host": {
"defaultMessage": "adres przekierowania"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domena docelowa"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Kod HTTP"
},
"redirection-hosts": {
"defaultMessage": "Przekierowania"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {przekierowanie} few {przekierowania} many {przekierowań} other {przekierowań}}"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standardowy użytkownik"
},
"save": {
"defaultMessage": "Zapisz"
},
"setting": {
"defaultMessage": "Ustawienie"
},
"settings": {
"defaultMessage": "Ustawienia"
},
"settings.default-site": {
"defaultMessage": "Domyślna strona"
},
"settings.default-site.404": {
"defaultMessage": "Strona 404"
},
"settings.default-site.444": {
"defaultMessage": "Brak odpowiedzi (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Strona gratulacyjna"
},
"settings.default-site.description": {
"defaultMessage": "Co wyświetlić, gdy Nginx otrzyma nieznany Host"
},
"settings.default-site.html": {
"defaultMessage": "Własny HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Przekierowanie"
},
"setup.preamble": {
"defaultMessage": "Zacznij od utworzenia konta administratora."
},
"setup.title": {
"defaultMessage": "Witaj!"
},
"sign-in": {
"defaultMessage": "Zaloguj się"
},
"ssl-certificate": {
"defaultMessage": "Certyfikat SSL"
},
"stream": {
"defaultMessage": "strumień"
},
"stream.forward-host": {
"defaultMessage": "Host docelowy"
},
"stream.incoming-port": {
"defaultMessage": "Port przychodzący"
},
"streams": {
"defaultMessage": "Strumienie"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {strumień} few {strumienie} many {strumieni} other {strumieni}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "użytkownik"
},
"user.change-password": {
"defaultMessage": "Zmień hasło"
},
"user.confirm-password": {
"defaultMessage": "Potwierdź nowe hasło"
},
"user.current-password": {
"defaultMessage": "Aktualne hasło"
},
"user.edit-profile": {
"defaultMessage": "Edytuj profil"
},
"user.full-name": {
"defaultMessage": "Imię / Nazwisko"
},
"user.login-as": {
"defaultMessage": "Zaloguj jako {name}"
},
"user.logout": {
"defaultMessage": "Wyloguj"
},
"user.new-password": {
"defaultMessage": "Nowe hasło"
},
"user.nickname": {
"defaultMessage": "Pseudonim"
},
"user.set-password": {
"defaultMessage": "Ustaw hasło"
},
"user.set-permissions": {
"defaultMessage": "Ustaw uprawnienia dla {name}"
},
"user.switch-dark": {
"defaultMessage": "Przełącz na tryb ciemny"
},
"user.switch-light": {
"defaultMessage": "Przełącz na tryb jasny"
},
"username": {
"defaultMessage": "Nazwa użytkownika"
},
"users": {
"defaultMessage": "Użytkownicy"
}
}
================================================
FILE: frontend/src/locale/src/pt.json
================================================
{
"access-list": {
"defaultMessage": "Lista de Controlo de Acesso (ACL)"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regra} other {Regras}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Utilizador} other {Utilizadores}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Quando existir pelo menos 1 regra, esta regra de negação geral será aplicada em último lugar"
},
"access-list.help.rules-order": {
"defaultMessage": "Nota: as diretivas allow e deny são aplicadas pela ordem em que forem definidas."
},
"access-list.pass-auth": {
"defaultMessage": "Passar Autenticação para o Upstream"
},
"access-list.public": {
"defaultMessage": "Acesso Público"
},
"access-list.public.subtitle": {
"defaultMessage": "Sem autenticação básica"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 ou 192.168.1.0/24 ou 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfazer Qualquer"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Utilizador} other {Utilizadores}}, {rules} {rules, plural, one {Regra} other {Regras}} – Criado em: {date}"
},
"access-lists": {
"defaultMessage": "Listas de Controlo de Acesso (ACL)"
},
"action.add": {
"defaultMessage": "Adicionar"
},
"action.add-location": {
"defaultMessage": "Adicionar Location"
},
"action.allow": {
"defaultMessage": "Permitir"
},
"action.close": {
"defaultMessage": "Fechar"
},
"action.delete": {
"defaultMessage": "Eliminar"
},
"action.deny": {
"defaultMessage": "Negar"
},
"action.disable": {
"defaultMessage": "Desativar"
},
"action.download": {
"defaultMessage": "Descarregar"
},
"action.edit": {
"defaultMessage": "Editar"
},
"action.enable": {
"defaultMessage": "Ativar"
},
"action.permissions": {
"defaultMessage": "Permissões"
},
"action.renew": {
"defaultMessage": "Renovar"
},
"action.view-details": {
"defaultMessage": "Ver Detalhes"
},
"auditlogs": {
"defaultMessage": "Registos de Auditoria"
},
"auto": {
"defaultMessage": "Automático"
},
"cancel": {
"defaultMessage": "Cancelar"
},
"certificate": {
"defaultMessage": "Certificado"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificado Personalizado"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Chave do Certificado"
},
"certificate.custom-intermediate": {
"defaultMessage": "Certificado Intermédio"
},
"certificate.in-use": {
"defaultMessage": "Em Utilização"
},
"certificate.none.subtitle": {
"defaultMessage": "Nenhum certificado atribuído"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Este host não irá utilizar HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Nenhum"
},
"certificate.not-in-use": {
"defaultMessage": "Não Utilizado"
},
"certificate.renew": {
"defaultMessage": "Renovar Certificado"
},
"certificates": {
"defaultMessage": "Certificados"
},
"certificates.custom": {
"defaultMessage": "Certificado Personalizado"
},
"certificates.custom.warning": {
"defaultMessage": "Ficheiros de chave protegidos por palavra-passe não são suportados."
},
"certificates.dns.credentials": {
"defaultMessage": "Conteúdo do Ficheiro de Credenciais"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Este plugin requer um ficheiro de configuração contendo um token API ou outras credenciais do fornecedor DNS."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Estes dados serão guardados em texto simples na base de dados e num ficheiro!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Segundos de Propagação"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Deixe em branco para usar o valor predefinido do plugin. Número de segundos a aguardar pela propagação DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Fornecedor DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Selecionar fornecedor..."
},
"certificates.dns.warning": {
"defaultMessage": "Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "O servidor está acessível e a criação de certificados deverá ser possível."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Resultados do Teste"
},
"certificates.http.warning": {
"defaultMessage": "Estes domínios devem já estar configurados para apontar para esta instalação."
},
"certificates.request.subtitle": {
"defaultMessage": "com o Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Pedir Novo Certificado"
},
"column.access": {
"defaultMessage": "Acesso"
},
"column.authorization": {
"defaultMessage": "Autorização"
},
"column.authorizations": {
"defaultMessage": "Autorizações"
},
"column.custom-locations": {
"defaultMessage": "Locations Personalizados"
},
"column.destination": {
"defaultMessage": "Destino"
},
"column.details": {
"defaultMessage": "Detalhes"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Evento"
},
"column.expires": {
"defaultMessage": "Expira"
},
"column.http-code": {
"defaultMessage": "Código HTTP"
},
"column.incoming-port": {
"defaultMessage": "Porta de Entrada"
},
"column.name": {
"defaultMessage": "Nome"
},
"column.protocol": {
"defaultMessage": "Protocolo"
},
"column.provider": {
"defaultMessage": "Fornecedor"
},
"column.roles": {
"defaultMessage": "Funções"
},
"column.rules": {
"defaultMessage": "Regras"
},
"column.satisfy": {
"defaultMessage": "Satisfazer"
},
"column.satisfy-all": {
"defaultMessage": "Todos"
},
"column.satisfy-any": {
"defaultMessage": "Qualquer"
},
"column.scheme": {
"defaultMessage": "Esquema"
},
"column.source": {
"defaultMessage": "Origem"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Estado"
},
"created-on": {
"defaultMessage": "Criado em: {date}"
},
"dashboard": {
"defaultMessage": "Painel"
},
"dead-host": {
"defaultMessage": "Host 404"
},
"dead-hosts": {
"defaultMessage": "Hosts 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}"
},
"disabled": {
"defaultMessage": "Desativado"
},
"domain-names": {
"defaultMessage": "Nomes de Domínio"
},
"domain-names.max": {
"defaultMessage": "Máximo de {count} domínios"
},
"domain-names.placeholder": {
"defaultMessage": "Comece a escrever para adicionar um domínio..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards não permitidos para este tipo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards não suportados por esta AC"
},
"domains.force-ssl": {
"defaultMessage": "Forçar SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Ativado"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS para Subdomínios"
},
"domains.http2-support": {
"defaultMessage": "Suporte HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Utilizar DNS Challenge"
},
"email-address": {
"defaultMessage": "Endereço de Email"
},
"empty-search": {
"defaultMessage": "Nenhum resultado encontrado"
},
"empty-subtitle": {
"defaultMessage": "Porque não cria um?"
},
"enabled": {
"defaultMessage": "Ativado"
},
"error.access.at-least-one": {
"defaultMessage": "É necessária pelo menos uma Autorização ou uma Regra de Acesso"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Os nomes de utilizador de autorização devem ser únicos"
},
"error.invalid-auth": {
"defaultMessage": "Email ou palavra-passe inválidos"
},
"error.invalid-domain": {
"defaultMessage": "Domínio inválido: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Endereço de email inválido"
},
"error.max-character-length": {
"defaultMessage": "Tamanho máximo: {max} caractere{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Demasiados domínios; o máximo é {max}"
},
"error.maximum": {
"defaultMessage": "Máximo permitido: {max}"
},
"error.min-character-length": {
"defaultMessage": "Tamanho mínimo: {min} caractere{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Mínimo permitido: {min}"
},
"error.passwords-must-match": {
"defaultMessage": "As palavras-passe têm de coincidir"
},
"error.required": {
"defaultMessage": "Campo obrigatório"
},
"expires.on": {
"defaultMessage": "Expira em: {date}"
},
"footer.github-fork": {
"defaultMessage": "Faz fork no GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Bloquear Exploits Comuns"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache de Conteúdos Estáticos"
},
"host.flags.preserve-path": {
"defaultMessage": "Preservar Caminho"
},
"host.flags.protocols": {
"defaultMessage": "Protocolos"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Suporte para WebSockets"
},
"host.forward-port": {
"defaultMessage": "Porta de Encaminhamento"
},
"host.forward-scheme": {
"defaultMessage": "Esquema"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "Apenas HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "A carregar…"
},
"login.title": {
"defaultMessage": "Iniciar sessão na sua conta"
},
"nginx-config.label": {
"defaultMessage": "Configuração Nginx Personalizada"
},
"nginx-config.placeholder": {
"defaultMessage": "# Insira aqui a sua configuração Nginx personalizada (utilize por sua conta e risco!)"
},
"no-permission-error": {
"defaultMessage": "Não tem permissões para ver esta página."
},
"notfound.action": {
"defaultMessage": "Voltar à página inicial"
},
"notfound.content": {
"defaultMessage": "A página que procura não foi encontrada."
},
"notfound.title": {
"defaultMessage": "Oops… Encontrou uma página de erro"
},
"notification.error": {
"defaultMessage": "Erro"
},
"notification.object-deleted": {
"defaultMessage": "{object} foi eliminado"
},
"notification.object-disabled": {
"defaultMessage": "{object} foi desativado"
},
"notification.object-enabled": {
"defaultMessage": "{object} foi ativado"
},
"notification.object-renewed": {
"defaultMessage": "{object} foi renovado"
},
"notification.object-saved": {
"defaultMessage": "{object} foi guardado"
},
"notification.success": {
"defaultMessage": "Sucesso"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Adicionar {object}"
},
"object.delete": {
"defaultMessage": "Eliminar {object}"
},
"object.delete.content": {
"defaultMessage": "Tem a certeza de que deseja eliminar este {object}?"
},
"object.edit": {
"defaultMessage": "Editar {object}"
},
"object.empty": {
"defaultMessage": "Não existem {objects}"
},
"object.event.created": {
"defaultMessage": "{object} criado"
},
"object.event.deleted": {
"defaultMessage": "{object} eliminado"
},
"object.event.disabled": {
"defaultMessage": "{object} desativado"
},
"object.event.enabled": {
"defaultMessage": "{object} ativado"
},
"object.event.renewed": {
"defaultMessage": "{object} renovado"
},
"object.event.updated": {
"defaultMessage": "{object} atualizado"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opções"
},
"password": {
"defaultMessage": "Palavra-passe"
},
"password.generate": {
"defaultMessage": "Gerar palavra-passe aleatória"
},
"password.hide": {
"defaultMessage": "Esconder Palavra-passe"
},
"password.show": {
"defaultMessage": "Mostrar Palavra-passe"
},
"permissions.hidden": {
"defaultMessage": "Oculto"
},
"permissions.manage": {
"defaultMessage": "Gerir"
},
"permissions.view": {
"defaultMessage": "Apenas Visualização"
},
"permissions.visibility.all": {
"defaultMessage": "Todos os Itens"
},
"permissions.visibility.title": {
"defaultMessage": "Visibilidade do Item"
},
"permissions.visibility.user": {
"defaultMessage": "Apenas Itens Criados"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Hostname/IP de Encaminhamento"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Público"
},
"redirection-host": {
"defaultMessage": "Host de Redirecionamento"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domínio de Destino"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Código HTTP"
},
"redirection-hosts": {
"defaultMessage": "Hosts de Redirecionamento"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host de Redirecionamento} other {Hosts de Redirecionamento}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Múltiplas Escolhas"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Movido Permanentemente"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Movido Temporariamente"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Ver Outro"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Redirecionamento Temporário"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Redirecionamento Permanente"
},
"role.admin": {
"defaultMessage": "Administrador"
},
"role.standard-user": {
"defaultMessage": "Utilizador Comum"
},
"save": {
"defaultMessage": "Guardar"
},
"setting": {
"defaultMessage": "Definição"
},
"settings": {
"defaultMessage": "Definições"
},
"settings.default-site": {
"defaultMessage": "Site Predefinido"
},
"settings.default-site.404": {
"defaultMessage": "Página 404"
},
"settings.default-site.444": {
"defaultMessage": "Sem Resposta (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Página de Boas-vindas"
},
"settings.default-site.description": {
"defaultMessage": "O que apresentar quando o Nginx recebe um Host desconhecido"
},
"settings.default-site.html": {
"defaultMessage": "HTML Personalizado"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Redirecionar"
},
"setup.preamble": {
"defaultMessage": "Comece por criar a sua conta de administrador."
},
"setup.title": {
"defaultMessage": "Bem-vindo!"
},
"sign-in": {
"defaultMessage": "Iniciar Sessão"
},
"ssl-certificate": {
"defaultMessage": "Certificado SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Host de Destino"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com ou 10.0.0.1 ou 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Porta de Entrada"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Testar"
},
"update-available": {
"defaultMessage": "Atualização Disponível: {latestVersion}"
},
"user": {
"defaultMessage": "Utilizador"
},
"user.change-password": {
"defaultMessage": "Alterar Palavra-passe"
},
"user.confirm-password": {
"defaultMessage": "Confirmar Palavra-passe"
},
"user.current-password": {
"defaultMessage": "Palavra-passe Atual"
},
"user.edit-profile": {
"defaultMessage": "Editar Perfil"
},
"user.full-name": {
"defaultMessage": "Nome Completo"
},
"user.login-as": {
"defaultMessage": "Iniciar sessão como {name}"
},
"user.logout": {
"defaultMessage": "Terminar Sessão"
},
"user.new-password": {
"defaultMessage": "Nova Palavra-passe"
},
"user.nickname": {
"defaultMessage": "Alcunha"
},
"user.set-password": {
"defaultMessage": "Definir Palavra-passe"
},
"user.set-permissions": {
"defaultMessage": "Definir Permissões para {name}"
},
"user.switch-dark": {
"defaultMessage": "Ativar Modo Escuro"
},
"user.switch-light": {
"defaultMessage": "Ativar Modo Claro"
},
"username": {
"defaultMessage": "Nome de Utilizador"
},
"users": {
"defaultMessage": "Utilizadores"
}
}
================================================
FILE: frontend/src/locale/src/ru.json
================================================
{
"access-list": {
"defaultMessage": "Список доступа"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {правило} few {правила} many {правил} other {правила}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Если есть хотя бы одно правило, правило 'запретить всё' будет добавлено последним"
},
"access-list.help.rules-order": {
"defaultMessage": "Обратите внимание: разрешающие и запрещающие директивы применяются в порядке их определения."
},
"access-list.pass-auth": {
"defaultMessage": "Передавать авторизацию на upstream-сервер"
},
"access-list.public": {
"defaultMessage": "Общедоступный"
},
"access-list.public.subtitle": {
"defaultMessage": "Без аутентификации"
},
"access-list.satisfy-any": {
"defaultMessage": "Любое совпадение"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}, {rules} {rules, plural, one {правило} few {правила} many {правил} other {правила}} - создан: {date}"
},
"access-lists": {
"defaultMessage": "Списки доступа"
},
"action.add": {
"defaultMessage": "Добавить"
},
"action.add-location": {
"defaultMessage": "Добавить маршрут"
},
"action.close": {
"defaultMessage": "Закрыть"
},
"action.delete": {
"defaultMessage": "Удалить"
},
"action.disable": {
"defaultMessage": "Выключить"
},
"action.download": {
"defaultMessage": "Скачать"
},
"action.edit": {
"defaultMessage": "Изменить"
},
"action.enable": {
"defaultMessage": "Включить"
},
"action.permissions": {
"defaultMessage": "Разрешения"
},
"action.renew": {
"defaultMessage": "Продлить"
},
"action.view-details": {
"defaultMessage": "Просмотреть сведения"
},
"auditlogs": {
"defaultMessage": "Журнал аудита"
},
"cancel": {
"defaultMessage": "Отменить"
},
"certificate": {
"defaultMessage": "Сертификат"
},
"certificate.custom-certificate": {
"defaultMessage": "Сертификат"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Ключ сертификата"
},
"certificate.custom-intermediate": {
"defaultMessage": "Промежуточный сертификат"
},
"certificate.in-use": {
"defaultMessage": "Используется"
},
"certificate.none.subtitle": {
"defaultMessage": "Сертификат не назначен"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Этот хост не будет использовать HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Нет"
},
"certificate.not-in-use": {
"defaultMessage": "Не используется"
},
"certificate.renew": {
"defaultMessage": "Продлить сертификат"
},
"certificates": {
"defaultMessage": "Сертификаты"
},
"certificates.custom": {
"defaultMessage": "Свой сертификат"
},
"certificates.custom.warning": {
"defaultMessage": "Файлы ключей, защищённые паролем, не поддерживаются."
},
"certificates.dns.credentials": {
"defaultMessage": "Содержимое файла учётных данных"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Этот плагин требует файл конфигурации, содержащий API-токен или другие учётные данные вашего провайдера"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Эти данные будут храниться в незашифрованном виде в базе данных и файле!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Ожидание распространения (сек.)"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Оставьте пустым для значения по умолчанию плагина. Секунды ожидания распространения DNS."
},
"certificates.dns.provider": {
"defaultMessage": "DNS-провайдер"
},
"certificates.dns.warning": {
"defaultMessage": "Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов."
},
"certificates.http.reachability-404": {
"defaultMessage": "На этом домене найден сервер, но, похоже, это не Nginx Proxy Manager. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Не удалось проверить доступность из‑за ошибки связи с site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "На этом домене недоступен сервер. Убедитесь, что домен существует и указывает на IP-адрес, где запущен ваш экземпляр NPM, и при необходимости порт 80 проброшен на вашем роутере."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Сервер доступен, выпуск сертификатов возможен."
},
"certificates.http.reachability-other": {
"defaultMessage": "На этом домене найден сервер, но он вернул неожиданный статус‑код {code}. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "На этом домене найден сервер, но он вернул неожиданные данные. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
},
"certificates.http.test-results": {
"defaultMessage": "Результаты проверки"
},
"certificates.http.warning": {
"defaultMessage": "Эти домены должны быть настроены и указывать на этот экземпляр."
},
"certificates.key-type": {
"defaultMessage": "Тип ключа"
},
"certificates.key-type-description": {
"defaultMessage": "RSA широко совместим, ECDSA быстрее и безопаснее, но может не поддерживаться старыми системами"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "через Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Получить новый сертификат"
},
"column.access": {
"defaultMessage": "Доступ"
},
"column.authorization": {
"defaultMessage": "Авторизация"
},
"column.authorizations": {
"defaultMessage": "Авторизации"
},
"column.custom-locations": {
"defaultMessage": "Свои маршруты"
},
"column.destination": {
"defaultMessage": "Назначение"
},
"column.details": {
"defaultMessage": "Сведения"
},
"column.email": {
"defaultMessage": "Эл. почта"
},
"column.event": {
"defaultMessage": "Событие"
},
"column.expires": {
"defaultMessage": "Истекает"
},
"column.http-code": {
"defaultMessage": "HTTP-код"
},
"column.incoming-port": {
"defaultMessage": "Входящий порт"
},
"column.name": {
"defaultMessage": "Имя"
},
"column.protocol": {
"defaultMessage": "Протокол"
},
"column.provider": {
"defaultMessage": "Провайдер"
},
"column.roles": {
"defaultMessage": "Роли"
},
"column.rules": {
"defaultMessage": "Правила"
},
"column.satisfy": {
"defaultMessage": "Условия"
},
"column.satisfy-all": {
"defaultMessage": "Все"
},
"column.satisfy-any": {
"defaultMessage": "Любое"
},
"column.scheme": {
"defaultMessage": "Схема"
},
"column.source": {
"defaultMessage": "Источник"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Статус"
},
"created-on": {
"defaultMessage": "Создан: {date}"
},
"dashboard": {
"defaultMessage": "Обзор"
},
"dead-host": {
"defaultMessage": "404-хост"
},
"dead-hosts": {
"defaultMessage": "404-хосты"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404-хост} few {404-хоста} many {404-хостов} other {404-хоста}}"
},
"disabled": {
"defaultMessage": "Выключен"
},
"domain-names": {
"defaultMessage": "Домены"
},
"domain-names.max": {
"defaultMessage": "Максимум {count} доменов"
},
"domain-names.placeholder": {
"defaultMessage": "Начните ввод, чтобы добавить домен..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Подстановочные домены не разрешены для этого типа"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Подстановочные домены не поддерживаются этим центром сертификации"
},
"domains.force-ssl": {
"defaultMessage": "Всегда SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Поддержка HSTS"
},
"domains.hsts-subdomains": {
"defaultMessage": "Поддомены HSTS"
},
"domains.http2-support": {
"defaultMessage": "Поддержка HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Проверка через DNS"
},
"email-address": {
"defaultMessage": "Адрес эл. почты"
},
"empty-search": {
"defaultMessage": "Ничего не найдено"
},
"empty-subtitle": {
"defaultMessage": "Почему бы не создать его?"
},
"enabled": {
"defaultMessage": "Включён"
},
"error.access.at-least-one": {
"defaultMessage": "Требуется хотя бы одна авторизация или одно правило доступа"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Имена пользователей для авторизации должны быть уникальными"
},
"error.invalid-auth": {
"defaultMessage": "Неверный адрес эл. почты или пароль"
},
"error.invalid-domain": {
"defaultMessage": "Неверный домен: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Неверный адрес эл. почты"
},
"error.max-character-length": {
"defaultMessage": "Максимальная длина {max} {max, plural, one {символ} few {символа} many {символов} other {символа}}"
},
"error.max-domains": {
"defaultMessage": "Слишком много доменов, максимум {max}"
},
"error.maximum": {
"defaultMessage": "Максимум {max}"
},
"error.min-character-length": {
"defaultMessage": "Минимальная длина {min} {min, plural, one {символ} few {символа} many {символов} other {символа}}"
},
"error.minimum": {
"defaultMessage": "Минимум {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Пароли должны совпадать"
},
"error.required": {
"defaultMessage": "Обязательное поле"
},
"expires.on": {
"defaultMessage": "Истекает: {date}"
},
"footer.github-fork": {
"defaultMessage": "Сделать форк на GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Блокировать известные эксплойты"
},
"host.flags.cache-assets": {
"defaultMessage": "Кэшировать ресурсы"
},
"host.flags.preserve-path": {
"defaultMessage": "Сохранять путь"
},
"host.flags.protocols": {
"defaultMessage": "Протоколы"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Поддержка WebSocket"
},
"host.forward-port": {
"defaultMessage": "Порт перенаправления"
},
"host.forward-scheme": {
"defaultMessage": "Схема"
},
"hosts": {
"defaultMessage": "Хосты"
},
"http-only": {
"defaultMessage": "Только HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt через DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt через HTTP"
},
"loading": {
"defaultMessage": "Загрузка…"
},
"login.title": {
"defaultMessage": "Авторизация"
},
"nginx-config.label": {
"defaultMessage": "Своя Nginx-конфигурация"
},
"nginx-config.placeholder": {
"defaultMessage": "# Введите здесь свою Nginx-конфигурацию, будьте осторожны!"
},
"no-permission-error": {
"defaultMessage": "У вас нет доступа для просмотра."
},
"notfound.action": {
"defaultMessage": "Вернуться на главную"
},
"notfound.content": {
"defaultMessage": "Извините, но страница, которую вы ищете, не найдена"
},
"notfound.title": {
"defaultMessage": "Упс… Вы попали на страницу ошибки"
},
"notification.error": {
"defaultMessage": "Ошибка"
},
"notification.object-deleted": {
"defaultMessage": "{object} удален"
},
"notification.object-disabled": {
"defaultMessage": "{object} выключен"
},
"notification.object-enabled": {
"defaultMessage": "{object} включен"
},
"notification.object-renewed": {
"defaultMessage": "{object} продлен"
},
"notification.object-saved": {
"defaultMessage": "{object} сохранен"
},
"notification.success": {
"defaultMessage": "Успешно"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Добавить {object}"
},
"object.delete": {
"defaultMessage": "Удалить {object}"
},
"object.delete.content": {
"defaultMessage": "Вы уверены, что хотите удалить {object}?"
},
"object.edit": {
"defaultMessage": "Изменить {object}"
},
"object.empty": {
"defaultMessage": "{objects} отсутствуют"
},
"object.event.created": {
"defaultMessage": "{object} создан"
},
"object.event.deleted": {
"defaultMessage": "{object} удален"
},
"object.event.disabled": {
"defaultMessage": "{object} выключен"
},
"object.event.enabled": {
"defaultMessage": "{object} включен"
},
"object.event.renewed": {
"defaultMessage": "{object} продлен"
},
"object.event.updated": {
"defaultMessage": "{object} обновлен"
},
"offline": {
"defaultMessage": "Офлайн"
},
"online": {
"defaultMessage": "Онлайн"
},
"options": {
"defaultMessage": "Параметры"
},
"password": {
"defaultMessage": "Пароль"
},
"password.generate": {
"defaultMessage": "Сгенерировать случайный пароль"
},
"password.hide": {
"defaultMessage": "Скрыть пароль"
},
"password.show": {
"defaultMessage": "Показать пароль"
},
"permissions.hidden": {
"defaultMessage": "Скрыто"
},
"permissions.manage": {
"defaultMessage": "Управление"
},
"permissions.view": {
"defaultMessage": "Только просмотр"
},
"permissions.visibility.all": {
"defaultMessage": "Все элементы"
},
"permissions.visibility.title": {
"defaultMessage": "Видимость элементов"
},
"permissions.visibility.user": {
"defaultMessage": "Созданные элементы"
},
"proxy-host": {
"defaultMessage": "Прокси-хост"
},
"proxy-host.forward-host": {
"defaultMessage": "Хост / IP перенаправления"
},
"proxy-hosts": {
"defaultMessage": "Прокси-хосты"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {прокси-хост} few {прокси-хоста} many {прокси-хостов} other {прокси-хоста}}"
},
"public": {
"defaultMessage": "Общедоступный"
},
"redirection-host": {
"defaultMessage": "Редирект-хост"
},
"redirection-host.forward-domain": {
"defaultMessage": "Домен перенаправления"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP-код"
},
"redirection-hosts": {
"defaultMessage": "Редирект-хосты"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {редирект-хост} few {редирект-хоста} many {редирект-хостов} other {редирект-хоста}}"
},
"role.admin": {
"defaultMessage": "Администратор"
},
"role.standard-user": {
"defaultMessage": "Обычный пользователь"
},
"save": {
"defaultMessage": "Сохранить"
},
"setting": {
"defaultMessage": "Настройка"
},
"settings": {
"defaultMessage": "Настройки"
},
"settings.default-site": {
"defaultMessage": "Страница по умолчанию"
},
"settings.default-site.404": {
"defaultMessage": "404-страница"
},
"settings.default-site.444": {
"defaultMessage": "Нет ответа (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Страница поздравления"
},
"settings.default-site.description": {
"defaultMessage": "Что показывать, когда Nginx получает неизвестный хост"
},
"settings.default-site.html": {
"defaultMessage": "Свой HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Перенаправление"
},
"setup.preamble": {
"defaultMessage": "Начните с создания учётной записи администратора."
},
"setup.title": {
"defaultMessage": "Добро пожаловать!"
},
"sign-in": {
"defaultMessage": "Войти"
},
"ssl-certificate": {
"defaultMessage": "SSL-сертификат"
},
"stream": {
"defaultMessage": "Поток"
},
"stream.forward-host": {
"defaultMessage": "Хост перенаправления"
},
"stream.incoming-port": {
"defaultMessage": "Входящий порт"
},
"streams": {
"defaultMessage": "Потоки"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {поток} few {потока} many {потоков} other {потока}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Проверить"
},
"user": {
"defaultMessage": "Пользователь"
},
"user.change-password": {
"defaultMessage": "Изменить пароль"
},
"user.confirm-password": {
"defaultMessage": "Повторите пароль"
},
"user.current-password": {
"defaultMessage": "Текущий пароль"
},
"user.edit-profile": {
"defaultMessage": "Изменить профиль"
},
"user.full-name": {
"defaultMessage": "Полное имя"
},
"user.login-as": {
"defaultMessage": "Войти как {name}"
},
"user.logout": {
"defaultMessage": "Выйти"
},
"user.new-password": {
"defaultMessage": "Новый пароль"
},
"user.nickname": {
"defaultMessage": "Псевдоним"
},
"user.set-password": {
"defaultMessage": "Задать пароль"
},
"user.set-permissions": {
"defaultMessage": "Задать разрешения для {name}"
},
"user.switch-dark": {
"defaultMessage": "Включить тёмную тему"
},
"user.switch-light": {
"defaultMessage": "Включить светлую тему"
},
"username": {
"defaultMessage": "Имя пользователя"
},
"users": {
"defaultMessage": "Пользователи"
}
}
================================================
FILE: frontend/src/locale/src/sk.json
================================================
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Počet zostávajúcich záložných kódov: {count}"
},
"2fa.backup-warning": {
"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."
},
"2fa.disable": {
"defaultMessage": "Vypnúť dvojfaktorové overovanie"
},
"2fa.disable-confirm": {
"defaultMessage": "Vypnúť 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Vypnutím dvojfaktorového overovania sa zníži bezpečnosť vášho účtu."
},
"2fa.disabled": {
"defaultMessage": "Vypnuté"
},
"2fa.done": {
"defaultMessage": "Uložil som si svoje záložné kódy."
},
"2fa.enable": {
"defaultMessage": "Zapnúť dvojfaktorové overovanie"
},
"2fa.enabled": {
"defaultMessage": "Zapnuté"
},
"2fa.enter-code": {
"defaultMessage": "Zadajte overovací kód"
},
"2fa.enter-code-disable": {
"defaultMessage": "Zadajte overovací kód na vypnutie"
},
"2fa.regenerate": {
"defaultMessage": "Znova vytvoriť"
},
"2fa.regenerate-backup": {
"defaultMessage": "Znova vytvoriť záložné kódy"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Zadajte overovací kód, aby sa vytvorili nové záložné kódy. Vaše staré kódy budú neplatné."
},
"2fa.secret-key": {
"defaultMessage": "Tajný kľúč"
},
"2fa.setup-instructions": {
"defaultMessage": "Naskenujte tento QR kód pomocou svojej overovacej aplikácie alebo zadajte tajný kľúč ručne."
},
"2fa.status": {
"defaultMessage": "Stav"
},
"2fa.title": {
"defaultMessage": "Dvojfaktorové overenie"
},
"2fa.verify-enable": {
"defaultMessage": "Overiť a zapnúť"
},
"access-list": {
"defaultMessage": "zoznam prístupov"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {pravidlo} few {pravidlá} other {pravidiel}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {používateľ} few {používatelia} other {používateľov}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Keď existuje aspoň jedno pravidlo, toto pravidlo „zamietnuť všetko“ bude pridané ako posledné"
},
"access-list.help.rules-order": {
"defaultMessage": "Upozornenie: pravidlá povoliť a zamietnuť budú uplatňované v poradí, v akom sú definované."
},
"access-list.pass-auth": {
"defaultMessage": "Odoslať overenie na Upstream"
},
"access-list.public": {
"defaultMessage": "Verejne prístupné"
},
"access-list.public.subtitle": {
"defaultMessage": "Nie je potrebné základné overenie"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 alebo 192.168.1.0/24 alebo 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Splniť ktorékoľvek"
},
"access-list.subtitle": {
"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}"
},
"access-lists": {
"defaultMessage": "Zoznamy prístupov"
},
"action.add": {
"defaultMessage": "Pridať"
},
"action.add-location": {
"defaultMessage": "Pridať umiestnenie"
},
"action.allow": {
"defaultMessage": "Povoliť"
},
"action.close": {
"defaultMessage": "Zavrieť"
},
"action.delete": {
"defaultMessage": "Vymazať"
},
"action.deny": {
"defaultMessage": "Zamietnuť"
},
"action.disable": {
"defaultMessage": "Deaktivovať"
},
"action.download": {
"defaultMessage": "Stiahnuť"
},
"action.edit": {
"defaultMessage": "Upraviť"
},
"action.enable": {
"defaultMessage": "Aktivovať"
},
"action.permissions": {
"defaultMessage": "Oprávnenia"
},
"action.renew": {
"defaultMessage": "Obnoviť"
},
"action.view-details": {
"defaultMessage": "Zobraziť podrobnosti"
},
"auditlogs": {
"defaultMessage": "Záznamy auditu"
},
"auto": {
"defaultMessage": "Automaticky"
},
"cancel": {
"defaultMessage": "Zrušiť"
},
"certificate": {
"defaultMessage": "certifikát"
},
"certificate.custom-certificate": {
"defaultMessage": "Certifikát"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Kľúč certifikátu"
},
"certificate.custom-intermediate": {
"defaultMessage": "Sprostredkovateľský certifikát"
},
"certificate.in-use": {
"defaultMessage": "Používa sa"
},
"certificate.none.subtitle": {
"defaultMessage": "Nie je priradený žiadny certifikát"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Tento hostiteľ nebude používať HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Žiadny"
},
"certificate.not-in-use": {
"defaultMessage": "Nepoužíva sa"
},
"certificate.renew": {
"defaultMessage": "Obnoviť certifikát"
},
"certificates": {
"defaultMessage": "Certifikáty"
},
"certificates.custom": {
"defaultMessage": "Vlastný certifikát"
},
"certificates.custom.warning": {
"defaultMessage": "Súbory kľúčov chránené heslom nie sú podporované."
},
"certificates.dns.credentials": {
"defaultMessage": "Obsah súboru s prihlasovacími údajmi"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Tento doplnok vyžaduje konfiguračný súbor obsahujúci API token alebo iné prihlasovacie údaje vášho poskytovateľa"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Tieto údaje budú uložené v databáze a v súbore ako obyčajný text!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Propagácia v sekundách"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Ponechajte prázdne pre predvolenú hodnotu doplnku. Počet sekúnd, počas ktorých sa čaká na propagáciu DNS."
},
"certificates.dns.provider": {
"defaultMessage": "DNS poskytovateľ"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Vyberte poskytovateľa..."
},
"certificates.dns.warning": {
"defaultMessage": "Táto sekcia vyžaduje znalosť Certbotu a jeho DNS doplnkov. Prosím, pozrite si dokumentáciu príslušného doplnku."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Váš server je dostupný a vytvorenie certifikátu by malo byť možné."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Výsledky testu"
},
"certificates.http.warning": {
"defaultMessage": "Tieto domény musia byť už nakonfigurované tak, aby smerovali na túto inštaláciu."
},
"certificates.key-type": {
"defaultMessage": "Typ kľúča"
},
"certificates.key-type-description": {
"defaultMessage": "RSA je široko kompatibilný, ECDSA je rýchlejší a bezpečnejší, ale nemusí byť podporovaný staršími systémami"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "pomocou Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Vyžiadať nový certifikát"
},
"column.access": {
"defaultMessage": "Prístup"
},
"column.authorization": {
"defaultMessage": "Autorizácia"
},
"column.authorizations": {
"defaultMessage": "Autorizácie"
},
"column.custom-locations": {
"defaultMessage": "Vlastné umiestnenia"
},
"column.destination": {
"defaultMessage": "Cieľ"
},
"column.details": {
"defaultMessage": "Podrobnosti"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Udalosť"
},
"column.expires": {
"defaultMessage": "Platnosť do"
},
"column.http-code": {
"defaultMessage": "Prístup"
},
"column.incoming-port": {
"defaultMessage": "Vstupný port"
},
"column.name": {
"defaultMessage": "Názov"
},
"column.protocol": {
"defaultMessage": "Protokol"
},
"column.provider": {
"defaultMessage": "Poskytovateľ"
},
"column.roles": {
"defaultMessage": "Roly"
},
"column.rules": {
"defaultMessage": "Pravidlá"
},
"column.satisfy": {
"defaultMessage": "Splniť"
},
"column.satisfy-all": {
"defaultMessage": "Všetky"
},
"column.satisfy-any": {
"defaultMessage": "Ktorékoľvek"
},
"column.scheme": {
"defaultMessage": "Schéma"
},
"column.source": {
"defaultMessage": "Zdroj"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stav"
},
"created-on": {
"defaultMessage": "Vytvorené: {date}"
},
"dashboard": {
"defaultMessage": "Panel"
},
"dead-host": {
"defaultMessage": "404 hostiteľa"
},
"dead-hosts": {
"defaultMessage": "404 Hostitelia"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 hostiteľ} few {404 hostitelia} other {404 hostiteľov}}"
},
"disabled": {
"defaultMessage": "Deaktivované"
},
"domain-names": {
"defaultMessage": "Doménové mená"
},
"domain-names.max": {
"defaultMessage": "Maximálne {count} doménových mien"
},
"domain-names.placeholder": {
"defaultMessage": "Začnite písať pre pridanie domény..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards nie sú pre tento typ povolené"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards nie sú podporované pre túto certifikačnú autoritu"
},
"domains.force-ssl": {
"defaultMessage": "Vynútiť SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS povolené"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS pre subdomény"
},
"domains.http2-support": {
"defaultMessage": "Podpora HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Použiť DNS výzvu"
},
"email-address": {
"defaultMessage": "Emailová adresa"
},
"empty-search": {
"defaultMessage": "Nenašli sa žiadne výsledky"
},
"empty-subtitle": {
"defaultMessage": "Prečo nevytvoríte nejaký?"
},
"enabled": {
"defaultMessage": "Aktivované"
},
"error.access.at-least-one": {
"defaultMessage": "Je vyžadovaná aspoň jedna autorizácia alebo jedno prístupové pravidlo"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Používateľské mená pre autorizáciu musia byť jedinečné"
},
"error.invalid-auth": {
"defaultMessage": "Neplatný email alebo heslo"
},
"error.invalid-domain": {
"defaultMessage": "Neplatná doména: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Neplatná emailová adresa"
},
"error.max-character-length": {
"defaultMessage": "Maximálna dĺžka je {max} znak{max, plural, one {} few {y} other {ov}}"
},
"error.max-domains": {
"defaultMessage": "Príliš veľa domén, maximum je {max}"
},
"error.maximum": {
"defaultMessage": "Maximum je {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimálna dĺžka je {min} znak{min, plural, one {} few {y} other {ov}}"
},
"error.minimum": {
"defaultMessage": "Minimum je {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Heslá sa musia zhodovať"
},
"error.required": {
"defaultMessage": "Toto pole je povinné"
},
"expires.on": {
"defaultMessage": "Platnosť do: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forknite ma na GitHube"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokovať bežné exploity"
},
"host.flags.cache-assets": {
"defaultMessage": "Uložiť zdroje do vyrovnávacej pamäte"
},
"host.flags.preserve-path": {
"defaultMessage": "Zachovať cestu"
},
"host.flags.protocols": {
"defaultMessage": "Protokoly"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Podpora WebSockets"
},
"host.forward-port": {
"defaultMessage": "Port presmerovania"
},
"host.forward-scheme": {
"defaultMessage": "Schéma"
},
"hosts": {
"defaultMessage": "Hostitelia"
},
"http-only": {
"defaultMessage": "Len HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt cez DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt cez HTTP"
},
"loading": {
"defaultMessage": "Načítava sa…"
},
"login.2fa-code": {
"defaultMessage": "Overovací kód"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Vložiť kód"
},
"login.2fa-description": {
"defaultMessage": "Vložte kód z vašej overovacej aplikácie"
},
"login.2fa-title": {
"defaultMessage": "Dvoj-faktorové overenie"
},
"login.2fa-verify": {
"defaultMessage": "Overiť"
},
"login.title": {
"defaultMessage": "Prihláste sa do svojho účtu"
},
"nginx-config.label": {
"defaultMessage": "Vlastná Nginx konfigurácia"
},
"nginx-config.placeholder": {
"defaultMessage": "# Zadajte vlastnú Nginx konfiguráciu na vlastné riziko!"
},
"no-permission-error": {
"defaultMessage": "Nemáte oprávnenie na zobrazenie tohto obsahu."
},
"notfound.action": {
"defaultMessage": "Späť na hlavnú stránku"
},
"notfound.content": {
"defaultMessage": "Ľutujeme, stránka, ktorú hľadáte, nebola nájdená"
},
"notfound.title": {
"defaultMessage": "Ups… Našli ste chybovú stránku"
},
"notification.error": {
"defaultMessage": "Chyba"
},
"notification.object-deleted": {
"defaultMessage": "{object} bol odstránený"
},
"notification.object-disabled": {
"defaultMessage": "{object} bol deaktivovaný"
},
"notification.object-enabled": {
"defaultMessage": "{object} bol aktivovaný"
},
"notification.object-renewed": {
"defaultMessage": "{object} bol obnovený"
},
"notification.object-saved": {
"defaultMessage": "{object} bol uložený"
},
"notification.success": {
"defaultMessage": "Úspech"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Pridať {object}"
},
"object.delete": {
"defaultMessage": "Vymazať {object}"
},
"object.delete.content": {
"defaultMessage": "Naozaj chcete vymazať tento {object}?"
},
"object.edit": {
"defaultMessage": "Upraviť {object}"
},
"object.empty": {
"defaultMessage": "Nie sú {objects}"
},
"object.event.created": {
"defaultMessage": "Vytvorený {object}"
},
"object.event.deleted": {
"defaultMessage": "Vymazaný {object}"
},
"object.event.disabled": {
"defaultMessage": "Deaktivovaný {object}"
},
"object.event.enabled": {
"defaultMessage": "Aktivovaný {object}"
},
"object.event.renewed": {
"defaultMessage": "Obnovený {object}"
},
"object.event.updated": {
"defaultMessage": "Aktualizovaný {object}"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Možnosti"
},
"password": {
"defaultMessage": "Heslo"
},
"password.generate": {
"defaultMessage": "Vygenerovať náhodné heslo"
},
"password.hide": {
"defaultMessage": "Skryť heslo"
},
"password.show": {
"defaultMessage": "Zobraziť heslo"
},
"permissions.hidden": {
"defaultMessage": "Skryté"
},
"permissions.manage": {
"defaultMessage": "Spravovať"
},
"permissions.view": {
"defaultMessage": "Len na zobrazenie"
},
"permissions.visibility.all": {
"defaultMessage": "Všetky položky"
},
"permissions.visibility.title": {
"defaultMessage": "Viditeľnosť položky"
},
"permissions.visibility.user": {
"defaultMessage": "Len vytvorené položky"
},
"proxy-host": {
"defaultMessage": "proxy hostiteľa"
},
"proxy-host.forward-host": {
"defaultMessage": "Cieľový názov hostiteľa / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy hostitelia"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {proxy hostiteľ} few {proxy hostitelia} other {proxy hostiteľov}}"
},
"public": {
"defaultMessage": "Verejné"
},
"redirection-host": {
"defaultMessage": "presmerovacieho hostiteľa"
},
"redirection-host.forward-domain": {
"defaultMessage": "Cieľová doména"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP kód"
},
"redirection-hosts": {
"defaultMessage": "Presmerovací hostitelia"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {presmerovací hostiteľ} few {presmerovací hostitelia} other {presmerovacích hostiteľov}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Viacero možností"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Trvalo presunuté"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Dočasne presunuté"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Pozrieť iné"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Dočasné presmerovanie"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Trvalé presmerovanie"
},
"role.admin": {
"defaultMessage": "Administrátor"
},
"role.standard-user": {
"defaultMessage": "Bežný používateľ"
},
"save": {
"defaultMessage": "Uložiť"
},
"setting": {
"defaultMessage": "Nastavenie"
},
"settings": {
"defaultMessage": "Nastavenia"
},
"settings.default-site": {
"defaultMessage": "Predvolená stránka"
},
"settings.default-site.404": {
"defaultMessage": "Stránka 404"
},
"settings.default-site.444": {
"defaultMessage": "Bez odpovede (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Gratulačná stránka"
},
"settings.default-site.description": {
"defaultMessage": "Čo zobraziť, keď Nginx zachytí neznámeho hostiteľa"
},
"settings.default-site.html": {
"defaultMessage": "Vlastné HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Presmerovať"
},
"setup.preamble": {
"defaultMessage": "Začnite vytvorením administrátorského účtu."
},
"setup.title": {
"defaultMessage": "Vitajte!"
},
"sign-in": {
"defaultMessage": "Prihlásiť sa"
},
"ssl-certificate": {
"defaultMessage": "SSL certifikát"
},
"stream": {
"defaultMessage": "stream"
},
"stream.forward-host": {
"defaultMessage": "Cieľový hostiteľ"
},
"stream.forward-host.placeholder": {
"defaultMessage": "napriklad.sk alebo 10.0.0.1 alebo 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Vstupný port"
},
"streams": {
"defaultMessage": "Streamy"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {stream} few {streamy} other {streamov}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Dostupná aktualizácia: {latestVersion}"
},
"user": {
"defaultMessage": "používateľa"
},
"user.change-password": {
"defaultMessage": "Zmeniť heslo"
},
"user.confirm-password": {
"defaultMessage": "Potvrdiť heslo"
},
"user.current-password": {
"defaultMessage": "Aktuálne heslo"
},
"user.edit-profile": {
"defaultMessage": "Upraviť profil"
},
"user.full-name": {
"defaultMessage": "Celé meno"
},
"user.login-as": {
"defaultMessage": "Prihlásiť sa ako {name}"
},
"user.logout": {
"defaultMessage": "Odhlásiť sa"
},
"user.new-password": {
"defaultMessage": "Nové heslo"
},
"user.nickname": {
"defaultMessage": "Prezývka"
},
"user.set-password": {
"defaultMessage": "Nastaviť heslo"
},
"user.set-permissions": {
"defaultMessage": "Nastaviť oprávnenia pre {name}"
},
"user.switch-dark": {
"defaultMessage": "Prepnúť na tmavý režim"
},
"user.switch-light": {
"defaultMessage": "Prepnúť na svetlý režim"
},
"user.two-factor": {
"defaultMessage": "Dvojfakt. overenie"
},
"username": {
"defaultMessage": "Používateľské meno"
},
"users": {
"defaultMessage": "Používatelia"
}
}
================================================
FILE: frontend/src/locale/src/tr.json
================================================
{
"access-list": {
"defaultMessage": "Erişim Listesi"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Kural} other {Kural}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Kullanıcı} other {Kullanıcı}}"
},
"access-list.help-rules-last": {
"defaultMessage": "En az 1 kural mevcut olduğunda, bu tümünü reddet kuralı en son eklenir"
},
"access-list.help.rules-order": {
"defaultMessage": "İzin ver ve reddet direktiflerinin tanımlandıkları sırayla uygulanacağını unutmayın."
},
"access-list.pass-auth": {
"defaultMessage": "Kimlik Doğrulamayı Yukarı Akışa İlet"
},
"access-list.public": {
"defaultMessage": "Herkese Açık"
},
"access-list.public.subtitle": {
"defaultMessage": "Temel kimlik doğrulama gerekmez"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 veya 192.168.1.0/24 veya 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Herhangi Birini Karşıla"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Kullanıcı} other {Kullanıcı}}, {rules} {rules, plural, one {Kural} other {Kural}} - Oluşturuldu: {date}"
},
"access-lists": {
"defaultMessage": "Erişim Listeleri"
},
"action.add": {
"defaultMessage": "Ekle"
},
"action.add-location": {
"defaultMessage": "Konum Ekle"
},
"action.allow": {
"defaultMessage": "İzin Ver"
},
"action.close": {
"defaultMessage": "Kapat"
},
"action.delete": {
"defaultMessage": "Sil"
},
"action.deny": {
"defaultMessage": "Reddet"
},
"action.disable": {
"defaultMessage": "Devre Dışı Bırak"
},
"action.download": {
"defaultMessage": "İndir"
},
"action.edit": {
"defaultMessage": "Düzenle"
},
"action.enable": {
"defaultMessage": "Etkinleştir"
},
"action.permissions": {
"defaultMessage": "İzinler"
},
"action.renew": {
"defaultMessage": "Yenile"
},
"action.view-details": {
"defaultMessage": "Detayları Görüntüle"
},
"auditlogs": {
"defaultMessage": "Denetim Kayıtları"
},
"auto": {
"defaultMessage": "Otomatik"
},
"cancel": {
"defaultMessage": "İptal"
},
"certificate": {
"defaultMessage": "Sertifika"
},
"certificate.custom-certificate": {
"defaultMessage": "Sertifika"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Sertifika Anahtarı"
},
"certificate.custom-intermediate": {
"defaultMessage": "Ara Sertifika"
},
"certificate.in-use": {
"defaultMessage": "Kullanımda"
},
"certificate.none.subtitle": {
"defaultMessage": "Sertifika atanmamış"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Bu host HTTPS kullanmayacak"
},
"certificate.none.title": {
"defaultMessage": "Yok"
},
"certificate.not-in-use": {
"defaultMessage": "Kullanılmıyor"
},
"certificate.renew": {
"defaultMessage": "Sertifikayı Yenile"
},
"certificates": {
"defaultMessage": "Sertifikalar"
},
"certificates.custom": {
"defaultMessage": "Özel Sertifika"
},
"certificates.custom.warning": {
"defaultMessage": "Parola ile korumalı anahtar dosyaları desteklenmiyor."
},
"certificates.dns.credentials": {
"defaultMessage": "Kimlik Bilgileri Dosya İçeriği"
},
"certificates.dns.credentials-note": {
"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"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Bu veriler veritabanında ve bir dosyada düz metin olarak saklanacak!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Yayılma Saniyesi"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Eklentinin varsayılan değerini kullanmak için boş bırakın. DNS yayılması için beklenilecek saniye sayısı."
},
"certificates.dns.provider": {
"defaultMessage": "DNS Sağlayıcı"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Bir Sağlayıcı Seçin..."
},
"certificates.dns.warning": {
"defaultMessage": "Bu bölüm Certbot ve DNS eklentileri hakkında bazı bilgiler gerektirir. Lütfen ilgili eklenti dokümantasyonuna bakın."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Sunucunuz erişilebilir ve sertifika oluşturma mümkün olmalı."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Test Sonuçları"
},
"certificates.http.warning": {
"defaultMessage": "Bu alan adları zaten bu kuruluma işaret edecek şekilde yapılandırılmış olmalıdır."
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encrypt ile"
},
"certificates.request.title": {
"defaultMessage": "Yeni Sertifika İste"
},
"column.access": {
"defaultMessage": "Erişim"
},
"column.authorization": {
"defaultMessage": "Yetkilendirme"
},
"column.authorizations": {
"defaultMessage": "Yetkilendirmeler"
},
"column.custom-locations": {
"defaultMessage": "Özel Konumlar"
},
"column.destination": {
"defaultMessage": "Hedef"
},
"column.details": {
"defaultMessage": "Detaylar"
},
"column.email": {
"defaultMessage": "E-posta"
},
"column.event": {
"defaultMessage": "Olay"
},
"column.expires": {
"defaultMessage": "Sona Erer"
},
"column.http-code": {
"defaultMessage": "HTTP Kodu"
},
"column.incoming-port": {
"defaultMessage": "Gelen Port"
},
"column.name": {
"defaultMessage": "Ad"
},
"column.protocol": {
"defaultMessage": "Protokol"
},
"column.provider": {
"defaultMessage": "Sağlayıcı"
},
"column.roles": {
"defaultMessage": "Roller"
},
"column.rules": {
"defaultMessage": "Kurallar"
},
"column.satisfy": {
"defaultMessage": "Karşıla"
},
"column.satisfy-all": {
"defaultMessage": "Tümü"
},
"column.satisfy-any": {
"defaultMessage": "Herhangi Biri"
},
"column.scheme": {
"defaultMessage": "Şema"
},
"column.source": {
"defaultMessage": "Kaynak"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Durum"
},
"created-on": {
"defaultMessage": "Oluşturuldu: {date}"
},
"dashboard": {
"defaultMessage": "Kontrol Paneli"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Host'lar"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Host}}"
},
"disabled": {
"defaultMessage": "Devre Dışı"
},
"domain-names": {
"defaultMessage": "Alan Adları"
},
"domain-names.max": {
"defaultMessage": "Maksimum {count} alan adı"
},
"domain-names.placeholder": {
"defaultMessage": "Alan adı eklemek için yazmaya başlayın..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Bu tür için joker karakterler izin verilmez"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Bu CA için joker karakterler desteklenmiyor"
},
"domains.force-ssl": {
"defaultMessage": "SSL'i Zorla"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Etkin"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Alt Alan Adları"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Desteği"
},
"domains.use-dns": {
"defaultMessage": "DNS Challenge Kullan"
},
"email-address": {
"defaultMessage": "E-posta adresi"
},
"empty-search": {
"defaultMessage": "Sonuç bulunamadı"
},
"empty-subtitle": {
"defaultMessage": "Neden bir tane oluşturmuyorsunuz?"
},
"enabled": {
"defaultMessage": "Etkin"
},
"error.access.at-least-one": {
"defaultMessage": "Ya bir Yetkilendirme ya da bir Erişim Kuralı gereklidir"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Yetkilendirme Kullanıcı Adları benzersiz olmalıdır"
},
"error.invalid-auth": {
"defaultMessage": "Geçersiz e-posta veya şifre"
},
"error.invalid-domain": {
"defaultMessage": "Geçersiz alan adı: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Geçersiz e-posta adresi"
},
"error.max-character-length": {
"defaultMessage": "Maksimum uzunluk {max} karakter{max, plural, one {} other {}}"
},
"error.max-domains": {
"defaultMessage": "Çok fazla alan adı, maksimum {max}"
},
"error.maximum": {
"defaultMessage": "Maksimum {max}"
},
"error.min-character-length": {
"defaultMessage": "Minimum uzunluk {min} karakter{min, plural, one {} other {}}"
},
"error.minimum": {
"defaultMessage": "Minimum {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Şifreler eşleşmelidir"
},
"error.required": {
"defaultMessage": "Bu gereklidir"
},
"expires.on": {
"defaultMessage": "Sona Erer: {date}"
},
"footer.github-fork": {
"defaultMessage": "Github'da Fork Yap"
},
"host.flags.block-exploits": {
"defaultMessage": "Yaygın Saldırıları Engelle"
},
"host.flags.cache-assets": {
"defaultMessage": "Varlıkları Önbelleğe Al"
},
"host.flags.preserve-path": {
"defaultMessage": "Yolu Koru"
},
"host.flags.protocols": {
"defaultMessage": "Protokoller"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Desteği"
},
"host.forward-port": {
"defaultMessage": "İletme Portu"
},
"host.forward-scheme": {
"defaultMessage": "Şema"
},
"hosts": {
"defaultMessage": "Host'lar"
},
"http-only": {
"defaultMessage": "Sadece HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "DNS ile Let's Encrypt"
},
"lets-encrypt-via-http": {
"defaultMessage": "HTTP ile Let's Encrypt"
},
"loading": {
"defaultMessage": "Yükleniyor…"
},
"login.title": {
"defaultMessage": "Hesabınıza giriş yapın"
},
"nginx-config.label": {
"defaultMessage": "Özel Nginx Yapılandırması"
},
"nginx-config.placeholder": {
"defaultMessage": "# Kendi riskinizle özel Nginx yapılandırmanızı buraya girin!"
},
"no-permission-error": {
"defaultMessage": "Bunu görüntüleme erişiminiz yok."
},
"notfound.action": {
"defaultMessage": "Ana sayfaya götür"
},
"notfound.content": {
"defaultMessage": "Üzgünüz, aradığınız sayfa bulunamadı"
},
"notfound.title": {
"defaultMessage": "Hata… Bir hata sayfası buldunuz"
},
"notification.error": {
"defaultMessage": "Hata"
},
"notification.object-deleted": {
"defaultMessage": "{object} silindi"
},
"notification.object-disabled": {
"defaultMessage": "{object} devre dışı bırakıldı"
},
"notification.object-enabled": {
"defaultMessage": "{object} etkinleştirildi"
},
"notification.object-renewed": {
"defaultMessage": "{object} yenilendi"
},
"notification.object-saved": {
"defaultMessage": "{object} kaydedildi"
},
"notification.success": {
"defaultMessage": "Başarılı"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object} Ekle"
},
"object.delete": {
"defaultMessage": "{object} Sil"
},
"object.delete.content": {
"defaultMessage": "Bu {object} öğesini silmek istediğinizden emin misiniz?"
},
"object.edit": {
"defaultMessage": "{object} Düzenle"
},
"object.empty": {
"defaultMessage": "Hiç {objects} yok"
},
"object.event.created": {
"defaultMessage": "{object} oluşturuldu"
},
"object.event.deleted": {
"defaultMessage": "{object} silindi"
},
"object.event.disabled": {
"defaultMessage": "{object} devre dışı bırakıldı"
},
"object.event.enabled": {
"defaultMessage": "{object} etkinleştirildi"
},
"object.event.renewed": {
"defaultMessage": "{object} yenilendi"
},
"object.event.updated": {
"defaultMessage": "{object} güncellendi"
},
"offline": {
"defaultMessage": "Çevrimdışı"
},
"online": {
"defaultMessage": "Çevrimiçi"
},
"options": {
"defaultMessage": "Seçenekler"
},
"password": {
"defaultMessage": "Şifre"
},
"password.generate": {
"defaultMessage": "Rastgele şifre oluştur"
},
"password.hide": {
"defaultMessage": "Şifreyi Gizle"
},
"password.show": {
"defaultMessage": "Şifreyi Göster"
},
"permissions.hidden": {
"defaultMessage": "Gizli"
},
"permissions.manage": {
"defaultMessage": "Yönet"
},
"permissions.view": {
"defaultMessage": "Sadece Görüntüle"
},
"permissions.visibility.all": {
"defaultMessage": "Tüm Öğeler"
},
"permissions.visibility.title": {
"defaultMessage": "Öğe Görünürlüğü"
},
"permissions.visibility.user": {
"defaultMessage": "Sadece Oluşturulan Öğeler"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "İletme Host Adı / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Host'lar"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Host}}"
},
"public": {
"defaultMessage": "Herkese Açık"
},
"redirection-host": {
"defaultMessage": "Yönlendirme Host'u"
},
"redirection-host.forward-domain": {
"defaultMessage": "İletme Alan Adı"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Kodu"
},
"redirection-hosts": {
"defaultMessage": "Yönlendirme Host'ları"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Yönlendirme Host'u} other {Yönlendirme Host'u}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Çoklu Seçenek"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Kalıcı olarak taşındı"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Geçici olarak taşındı"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Diğerini gör"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Geçici yönlendirme"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Kalıcı yönlendirme"
},
"role.admin": {
"defaultMessage": "Yönetici"
},
"role.standard-user": {
"defaultMessage": "Standart Kullanıcı"
},
"save": {
"defaultMessage": "Kaydet"
},
"setting": {
"defaultMessage": "Ayar"
},
"settings": {
"defaultMessage": "Ayarlar"
},
"settings.default-site": {
"defaultMessage": "Varsayılan Site"
},
"settings.default-site.404": {
"defaultMessage": "404 Sayfası"
},
"settings.default-site.444": {
"defaultMessage": "Yanıt Yok (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Tebrikler Sayfası"
},
"settings.default-site.description": {
"defaultMessage": "Nginx bilinmeyen bir Host ile karşılaştığında ne gösterilecek"
},
"settings.default-site.html": {
"defaultMessage": "Özel HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Yönlendir"
},
"setup.preamble": {
"defaultMessage": "Yönetici hesabınızı oluşturarak başlayın."
},
"setup.title": {
"defaultMessage": "Hoş Geldiniz!"
},
"sign-in": {
"defaultMessage": "Giriş yap"
},
"ssl-certificate": {
"defaultMessage": "SSL Sertifikası"
},
"stream": {
"defaultMessage": "Akış"
},
"stream.forward-host": {
"defaultMessage": "İletme Host'u"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com veya 10.0.0.1 veya 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Gelen Port"
},
"streams": {
"defaultMessage": "Akışlar"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Akış} other {Akış}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"update-available": {
"defaultMessage": "Güncelleme Mevcut: {latestVersion}"
},
"user": {
"defaultMessage": "Kullanıcı"
},
"user.change-password": {
"defaultMessage": "Şifreyi Değiştir"
},
"user.confirm-password": {
"defaultMessage": "Şifreyi Onayla"
},
"user.current-password": {
"defaultMessage": "Mevcut Şifre"
},
"user.edit-profile": {
"defaultMessage": "Profili Düzenle"
},
"user.full-name": {
"defaultMessage": "Ad Soyad"
},
"user.login-as": {
"defaultMessage": "{name} olarak giriş yap"
},
"user.logout": {
"defaultMessage": "Çıkış Yap"
},
"user.new-password": {
"defaultMessage": "Yeni Şifre"
},
"user.nickname": {
"defaultMessage": "Takma Ad"
},
"user.set-password": {
"defaultMessage": "Şifre Belirle"
},
"user.set-permissions": {
"defaultMessage": "{name} için İzinleri Belirle"
},
"user.switch-dark": {
"defaultMessage": "Karanlık moda geç"
},
"user.switch-light": {
"defaultMessage": "Açık moda geç"
},
"username": {
"defaultMessage": "Kullanıcı Adı"
},
"users": {
"defaultMessage": "Kullanıcılar"
}
}
================================================
FILE: frontend/src/locale/src/vi.json
================================================
{
"access-list": {
"defaultMessage": "Danh sách truy cập"
},
"access-list.access-count": {
"defaultMessage": "{count} quy tắc"
},
"access-list.auth-count": {
"defaultMessage": "{count} người dùng"
},
"access-list.help-rules-last": {
"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"
},
"access-list.help.rules-order": {
"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."
},
"access-list.pass-auth": {
"defaultMessage": "Chuyển xác thực lên thượng nguồn"
},
"access-list.public": {
"defaultMessage": "Có thể truy cập công khai"
},
"access-list.public.subtitle": {
"defaultMessage": "Không cần xác thực cơ bản"
},
"access-list.satisfy-any": {
"defaultMessage": "Thỏa mãn điều kiện bất kỳ"
},
"access-list.subtitle": {
"defaultMessage": "{users} người dùng, {rules} quy tắc - Tạo lúc: {date}"
},
"access-lists": {
"defaultMessage": "Danh sách truy cập"
},
"action.add": {
"defaultMessage": "Thêm"
},
"action.add-location": {
"defaultMessage": "Thêm Vị trí"
},
"action.close": {
"defaultMessage": "Đóng"
},
"action.delete": {
"defaultMessage": "Xóa"
},
"action.disable": {
"defaultMessage": "Tắt"
},
"action.download": {
"defaultMessage": "Tải xuống"
},
"action.edit": {
"defaultMessage": "Chỉnh sửa"
},
"action.enable": {
"defaultMessage": "Bật"
},
"action.permissions": {
"defaultMessage": "Quyền"
},
"action.renew": {
"defaultMessage": "Gia hạn"
},
"action.view-details": {
"defaultMessage": "Xem Chi tiết"
},
"auditlogs": {
"defaultMessage": "Nhật ký kiểm tra"
},
"cancel": {
"defaultMessage": "Hủy"
},
"certificate": {
"defaultMessage": "Chứng chỉ"
},
"certificate.custom-certificate": {
"defaultMessage": "Certificate (crt)"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Certificate Key"
},
"certificate.custom-intermediate": {
"defaultMessage": "Intermediate Certificate"
},
"certificate.in-use": {
"defaultMessage": "Đang sử dụng"
},
"certificate.none.subtitle": {
"defaultMessage": "Không có chứng chỉ nào được chỉ định"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Máy chủ này sẽ không sử dụng HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Không có"
},
"certificate.not-in-use": {
"defaultMessage": "Không được dùng"
},
"certificate.renew": {
"defaultMessage": "Gia hạn Chứng chỉ"
},
"certificates": {
"defaultMessage": "Danh sách chứng chỉ"
},
"certificates.custom": {
"defaultMessage": "Chứng chỉ tùy chỉnh"
},
"certificates.custom.warning": {
"defaultMessage": "Các tệp chính được bảo vệ bằng cụm mật khẩu không được hỗ trợ."
},
"certificates.dns.credentials": {
"defaultMessage": "Nội dung tệp thông tin xác thực"
},
"certificates.dns.credentials-note": {
"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"
},
"certificates.dns.credentials-warning": {
"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!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Thời gian lan truyền (Giây)"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Để trống để sử dụng giá trị mặc định của plugin. Số giây chờ truyền DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Nhà cung cấp DNS"
},
"certificates.dns.warning": {
"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."
},
"certificates.http.reachability-404": {
"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."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Không thể kiểm tra khả năng truy cập do lỗi giao tiếp với site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"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."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Máy chủ của bạn có thể truy cập được và có thể tạo chứng chỉ."
},
"certificates.http.reachability-other": {
"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."
},
"certificates.http.reachability-wrong-data": {
"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."
},
"certificates.http.test-results": {
"defaultMessage": "Kết quả kiểm tra"
},
"certificates.http.warning": {
"defaultMessage": "Các miền này phải được cấu hình sẵn để trỏ đến cài đặt này."
},
"certificates.key-type": {
"defaultMessage": "Loại khóa"
},
"certificates.key-type-description": {
"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ũ"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "bằng Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Yêu cầu chứng chỉ mới"
},
"column.access": {
"defaultMessage": "Truy cập"
},
"column.authorization": {
"defaultMessage": "Ủy quyền"
},
"column.authorizations": {
"defaultMessage": "Danh sách ủy quyền"
},
"column.custom-locations": {
"defaultMessage": "Quy tắc đường dẫn tùy chỉnh (Vị trí)"
},
"column.destination": {
"defaultMessage": "Mục tiêu"
},
"column.details": {
"defaultMessage": "Chi tiết"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Sự kiện"
},
"column.expires": {
"defaultMessage": "Hết hạn"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Cổng đến"
},
"column.name": {
"defaultMessage": "Tên"
},
"column.protocol": {
"defaultMessage": "Giao thức"
},
"column.provider": {
"defaultMessage": "Nhà cung cấp"
},
"column.roles": {
"defaultMessage": "Vai trò"
},
"column.rules": {
"defaultMessage": "Quy tắc"
},
"column.satisfy": {
"defaultMessage": "Thỏa mãn"
},
"column.satisfy-all": {
"defaultMessage": "Tất cả"
},
"column.satisfy-any": {
"defaultMessage": "Bất kì"
},
"column.scheme": {
"defaultMessage": "Scheme"
},
"column.source": {
"defaultMessage": "Nguồn"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Trạng thái"
},
"created-on": {
"defaultMessage": "Đã tạo: {date}"
},
"dashboard": {
"defaultMessage": "Bảng điều khiển"
},
"dead-host": {
"defaultMessage": "Máy chủ 404"
},
"dead-hosts": {
"defaultMessage": "Máy chủ 404"
},
"dead-hosts.count": {
"defaultMessage": "Số trang lỗi {count}"
},
"disabled": {
"defaultMessage": "Đã tắt"
},
"domain-names": {
"defaultMessage": "Danh sách tên miền"
},
"domain-names.max": {
"defaultMessage": "Tối đa {count} tên miền"
},
"domain-names.placeholder": {
"defaultMessage": "Nhập tên miền vào đây..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Ký tự đại diện không được phép cho loại này"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Ký tự đại diện không được hỗ trợ cho CA này"
},
"domains.force-ssl": {
"defaultMessage": "Bắt buộc SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Bật HSTS"
},
"domains.hsts-subdomains": {
"defaultMessage": "Tên miền phụ HSTS"
},
"domains.http2-support": {
"defaultMessage": "Hỗ trợ HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Dùng thử thách DNS"
},
"email-address": {
"defaultMessage": "Địa chỉ email"
},
"empty-search": {
"defaultMessage": "Không có kết quả nào"
},
"empty-subtitle": {
"defaultMessage": "Tại sao bạn không tạo một cái luôn?"
},
"enabled": {
"defaultMessage": "Đã bật"
},
"error.access.at-least-one": {
"defaultMessage": "Yêu cầu ít nhất một quy tắc ủy quyền hoặc truy cập"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Tên người dùng được ủy quyền phải là duy nhất"
},
"error.invalid-auth": {
"defaultMessage": "Email hoặc Mật khẩu không hợp lệ"
},
"error.invalid-domain": {
"defaultMessage": "Tên miền không hợp lệ: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Địa chỉ email không hợp lệ"
},
"error.max-character-length": {
"defaultMessage": "Độ dài tối đa là {max} ký tự"
},
"error.max-domains": {
"defaultMessage": "Quá nhiều tên miền, tối đa là {max}"
},
"error.maximum": {
"defaultMessage": "Tối đa là {max}"
},
"error.min-character-length": {
"defaultMessage": "Độ dài tối thiểu là {min} ký tự"
},
"error.minimum": {
"defaultMessage": "Tối thiểu là {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Mật khẩu phải khớp"
},
"error.required": {
"defaultMessage": "Điều này là bắt buộc"
},
"expires.on": {
"defaultMessage": "Hết hạn: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork dự án này trên Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Chặn các hoạt động khai thác phổ biến"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache tài nguyên"
},
"host.flags.preserve-path": {
"defaultMessage": "Bảo toàn đường dẫn"
},
"host.flags.protocols": {
"defaultMessage": "Giao thức"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Hỗ trợ Websockets"
},
"host.forward-port": {
"defaultMessage": "Chuyển tiếp cổng"
},
"host.forward-scheme": {
"defaultMessage": "Scheme"
},
"hosts": {
"defaultMessage": "Máy chủ"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt qua DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt qua HTTP"
},
"loading": {
"defaultMessage": "Đang tải..."
},
"login.title": {
"defaultMessage": "Đăng nhập vào tài khoản của bạn"
},
"nginx-config.label": {
"defaultMessage": "Cấu hình Nginx tùy chỉnh"
},
"nginx-config.placeholder": {
"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!"
},
"no-permission-error": {
"defaultMessage": "Bạn không có quyền truy cập trang này."
},
"notfound.action": {
"defaultMessage": "Về trang chủ"
},
"notfound.content": {
"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"
},
"notfound.title": {
"defaultMessage": "Rất tiếc… Bạn vừa tìm thấy một trang lỗi"
},
"notification.error": {
"defaultMessage": "Lỗi"
},
"notification.object-deleted": {
"defaultMessage": "{object} đã được xóa"
},
"notification.object-disabled": {
"defaultMessage": "{object} đã được tắt"
},
"notification.object-enabled": {
"defaultMessage": "{object} đã được bật"
},
"notification.object-renewed": {
"defaultMessage": "{object} đã được làm mới"
},
"notification.object-saved": {
"defaultMessage": "{object} đã được lưu"
},
"notification.success": {
"defaultMessage": "Thành công"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Thêm {object}"
},
"object.delete": {
"defaultMessage": "Xóa {object}"
},
"object.delete.content": {
"defaultMessage": "Bạn có chắc muốn xóa {object} không?"
},
"object.edit": {
"defaultMessage": "Chỉnh sửa {object}"
},
"object.empty": {
"defaultMessage": "Không có {objects}"
},
"object.event.created": {
"defaultMessage": "Đã tạo {object}"
},
"object.event.deleted": {
"defaultMessage": "Đã xóa {object}"
},
"object.event.disabled": {
"defaultMessage": "Đã tắt {object}"
},
"object.event.enabled": {
"defaultMessage": "Đã bật {object}"
},
"object.event.renewed": {
"defaultMessage": "Đã gia hạn {object}"
},
"object.event.updated": {
"defaultMessage": "Đã cập nhật {object}"
},
"offline": {
"defaultMessage": "Ngoại tuyến"
},
"online": {
"defaultMessage": "Trực tuyến"
},
"options": {
"defaultMessage": "Tùy chọn"
},
"password": {
"defaultMessage": "Mật khẩu"
},
"password.generate": {
"defaultMessage": "Tạo mật khẩu ngẫu nhiên"
},
"password.hide": {
"defaultMessage": "Ẩn Mật khẩu"
},
"password.show": {
"defaultMessage": "Hiện Mật khẩu"
},
"permissions.hidden": {
"defaultMessage": "Ẩn"
},
"permissions.manage": {
"defaultMessage": "Quản lý"
},
"permissions.view": {
"defaultMessage": "Chỉ xem"
},
"permissions.visibility.all": {
"defaultMessage": "Tất cả các mục"
},
"permissions.visibility.title": {
"defaultMessage": "Khả năng hiển thị mục"
},
"permissions.visibility.user": {
"defaultMessage": "Chỉ các mục đã tạo"
},
"proxy-host": {
"defaultMessage": "Máy chủ proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Chuyển tiếp Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Máy chủ proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} máy chủ proxy"
},
"public": {
"defaultMessage": "Công khai"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Chuyển tiếp Tên miền"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} máy chủ chuyển hướng"
},
"role.admin": {
"defaultMessage": "Quản trị viên"
},
"role.standard-user": {
"defaultMessage": "Người dùng bình thường"
},
"save": {
"defaultMessage": "Lưu"
},
"setting": {
"defaultMessage": "Cài đặt"
},
"settings": {
"defaultMessage": "Cài đặt"
},
"settings.default-site": {
"defaultMessage": "Trang web mặc định"
},
"settings.default-site.404": {
"defaultMessage": "Trang 404"
},
"settings.default-site.444": {
"defaultMessage": "Không có phản hồi (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Trang chào mừng"
},
"settings.default-site.description": {
"defaultMessage": "Hiển thị gì khi Nginx gặp phải Máy chủ không xác định"
},
"settings.default-site.html": {
"defaultMessage": "HTML tùy chỉnh"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "Chuyển hướng"
},
"setup.preamble": {
"defaultMessage": "Bắt đầu bằng cách tạo tài khoản quản trị viên."
},
"setup.title": {
"defaultMessage": "Chào mừng!"
},
"sign-in": {
"defaultMessage": "Đăng nhập"
},
"ssl-certificate": {
"defaultMessage": "Chứng chỉ SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Chuyển tiếp Host"
},
"stream.incoming-port": {
"defaultMessage": "Cổng vào"
},
"streams": {
"defaultMessage": "Danh sách các Stream"
},
"streams.count": {
"defaultMessage": "Số Stream {count}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Kiểm tra"
},
"update-available": {
"defaultMessage": "Cập nhật khả dụng: {latestVersion}"
},
"user": {
"defaultMessage": "Người dùng"
},
"user.change-password": {
"defaultMessage": "Đổi Mật khẩu"
},
"user.confirm-password": {
"defaultMessage": "Xác nhận Mật khẩu"
},
"user.current-password": {
"defaultMessage": "Mật khẩu hiện tại"
},
"user.edit-profile": {
"defaultMessage": "Chỉnh sửa hồ sơ"
},
"user.full-name": {
"defaultMessage": "Tên"
},
"user.login-as": {
"defaultMessage": "Đăng nhập bằng {name}"
},
"user.logout": {
"defaultMessage": "Đăng xuất"
},
"user.new-password": {
"defaultMessage": "Mật khẩu mới"
},
"user.nickname": {
"defaultMessage": "Tên hiển thị"
},
"user.set-password": {
"defaultMessage": "Đặt Mật khẩu"
},
"user.set-permissions": {
"defaultMessage": "Đặt quyền cho {name}"
},
"user.switch-dark": {
"defaultMessage": "Chuyển sang chế độ tối"
},
"user.switch-light": {
"defaultMessage": "Chuyển sang chế độ sáng"
},
"username": {
"defaultMessage": "Tên người dùng"
},
"users": {
"defaultMessage": "Danh sách người dùng"
}
}
================================================
FILE: frontend/src/locale/src/zh.json
================================================
{
"access-list": {
"defaultMessage": "通信规则"
},
"access-list.access-count": {
"defaultMessage": "{count} 条规则"
},
"access-list.auth-count": {
"defaultMessage": "{count} 个用户"
},
"access-list.help-rules-last": {
"defaultMessage": "当至少存在1条规则时,此拒绝所有规则将被添加到最后"
},
"access-list.help.rules-order": {
"defaultMessage": " 允许 (allow) 和禁止 (deny) 规则将按照它们定义的顺序执行。"
},
"access-list.pass-auth": {
"defaultMessage": "将认证传递给上游"
},
"access-list.public": {
"defaultMessage": "公开可访问"
},
"access-list.public.subtitle": {
"defaultMessage": "无需基本认证"
},
"access-list.satisfy-any": {
"defaultMessage": "满足任意条件"
},
"access-list.subtitle": {
"defaultMessage": "{users} 个用户, {rules} 条规则 - 创建时间: {date}"
},
"access-lists": {
"defaultMessage": "通信规则"
},
"action.add": {
"defaultMessage": "添加"
},
"action.add-location": {
"defaultMessage": "添加路径规则(Location)"
},
"action.close": {
"defaultMessage": "关闭"
},
"action.delete": {
"defaultMessage": "删除"
},
"action.disable": {
"defaultMessage": "禁用"
},
"action.download": {
"defaultMessage": "下载"
},
"action.edit": {
"defaultMessage": "编辑"
},
"action.enable": {
"defaultMessage": "启用"
},
"action.permissions": {
"defaultMessage": "权限"
},
"action.renew": {
"defaultMessage": "续期"
},
"action.view-details": {
"defaultMessage": "查看详情"
},
"auditlogs": {
"defaultMessage": "审计日志"
},
"cancel": {
"defaultMessage": "取消"
},
"certificate": {
"defaultMessage": "证书"
},
"certificate.custom-certificate": {
"defaultMessage": "证书"
},
"certificate.custom-certificate-key": {
"defaultMessage": "证书密钥"
},
"certificate.custom-intermediate": {
"defaultMessage": "中间证书"
},
"certificate.in-use": {
"defaultMessage": "使用中"
},
"certificate.none.subtitle": {
"defaultMessage": "未分配证书"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "此主机将不使用 HTTPS"
},
"certificate.none.title": {
"defaultMessage": "无"
},
"certificate.not-in-use": {
"defaultMessage": "未使用"
},
"certificate.renew": {
"defaultMessage": "续期证书"
},
"certificates": {
"defaultMessage": "证书列表"
},
"certificates.custom": {
"defaultMessage": "自定义证书"
},
"certificates.custom.warning": {
"defaultMessage": "不支持受密码保护的密钥文件。"
},
"certificates.dns.credentials": {
"defaultMessage": "凭据文件内容"
},
"certificates.dns.credentials-note": {
"defaultMessage": "此插件需要一个包含 API 令牌或提供商其他凭证的配置文件"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "此数据将以明文形式存储在数据库和文件中!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "传播时间 (秒)"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "留空以使用插件默认值。等待DNS传播的秒数。"
},
"certificates.dns.provider": {
"defaultMessage": "DNS 提供商"
},
"certificates.dns.warning": {
"defaultMessage": "本节需要您具备一些关于 Certbot 及其 DNS 插件的知识,请参阅相应插件的官方文档。"
},
"certificates.http.reachability-404": {
"defaultMessage": "在此域名下找到了一个服务器,但它似乎不是 Nginx 代理管理器。请确保您的域名指向 NPM 实例运行的 IP 地址。"
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "由于与site24x7.com通信错误,无法检查可达性。"
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "此域名下没有可用的服务器。请确保您的域名存在并指向NPM实例运行的 IP 地址,如有必要,请在路由器中转发 80 端口。"
},
"certificates.http.reachability-ok": {
"defaultMessage": "您的服务器可以访问,应该可以创建证书。"
},
"certificates.http.reachability-other": {
"defaultMessage": "在此域名下找到了一个服务器,但它返回了意外的状态码 {code}。它是 NPM 服务器吗?请确保您的域名指向NPM实例运行的 IP 地址。"
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "在此域名下找到了一个服务器,但它返回了意外的数据。它是 NPM 服务器吗?请确保您的域名指向 NPM 实例运行的 IP 地址。"
},
"certificates.http.test-results": {
"defaultMessage": "测试结果"
},
"certificates.http.warning": {
"defaultMessage": "这些域名必须配置为指向本设备。"
},
"certificates.key-type": {
"defaultMessage": "密钥类型"
},
"certificates.key-type-description": {
"defaultMessage": "RSA 兼容性更好,ECDSA 更快更安全但旧系统可能不支持"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "使用 Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "申请新证书"
},
"column.access": {
"defaultMessage": "访问"
},
"column.authorization": {
"defaultMessage": "授权"
},
"column.authorizations": {
"defaultMessage": "授权列表"
},
"column.custom-locations": {
"defaultMessage": "自定义路径规则 (Locations)"
},
"column.destination": {
"defaultMessage": "目标"
},
"column.details": {
"defaultMessage": "详情"
},
"column.email": {
"defaultMessage": "邮箱"
},
"column.event": {
"defaultMessage": "事件"
},
"column.expires": {
"defaultMessage": "过期时间"
},
"column.http-code": {
"defaultMessage": "访问"
},
"column.incoming-port": {
"defaultMessage": "入站端口"
},
"column.name": {
"defaultMessage": "名称"
},
"column.protocol": {
"defaultMessage": "协议"
},
"column.provider": {
"defaultMessage": "提供商"
},
"column.roles": {
"defaultMessage": "角色"
},
"column.rules": {
"defaultMessage": "规则"
},
"column.satisfy": {
"defaultMessage": "满足"
},
"column.satisfy-all": {
"defaultMessage": "全部"
},
"column.satisfy-any": {
"defaultMessage": "任意"
},
"column.scheme": {
"defaultMessage": "协议"
},
"column.source": {
"defaultMessage": "来源"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "状态"
},
"created-on": {
"defaultMessage": "创建时间: {date}"
},
"dashboard": {
"defaultMessage": "仪表板"
},
"dead-host": {
"defaultMessage": "错误页面"
},
"dead-hosts": {
"defaultMessage": "错误页面列表"
},
"dead-hosts.count": {
"defaultMessage": "{count} 个错误页面列表"
},
"disabled": {
"defaultMessage": "已禁用"
},
"domain-names": {
"defaultMessage": "域名"
},
"domain-names.max": {
"defaultMessage": "{count} 个最多域名数量"
},
"domain-names.placeholder": {
"defaultMessage": "开始输入以添加域名..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "此类型不允许使用通配符"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "此 CA 不支持通配符"
},
"domains.advanced": {
"defaultMessage": "高级选项"
},
"domains.force-ssl": {
"defaultMessage": "强制 SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS 已启用"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS 子域名"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 支持"
},
"domains.trust-forwarded-proto": {
"defaultMessage": "信任上游代理传递的协议类型头"
},
"domains.use-dns": {
"defaultMessage": "使用DNS验证"
},
"email-address": {
"defaultMessage": "邮箱地址"
},
"empty-search": {
"defaultMessage": "未找到结果"
},
"empty-subtitle": {
"defaultMessage": "为什么不由您来创建一个呢?"
},
"enabled": {
"defaultMessage": "已启用"
},
"error.access.at-least-one": {
"defaultMessage": "需要至少一个授权或访问规则"
},
"error.access.duplicate-usernames": {
"defaultMessage": "授权用户名必须唯一"
},
"error.invalid-auth": {
"defaultMessage": "无效的邮箱或密码"
},
"error.invalid-domain": {
"defaultMessage": "无效的域名: {domain}"
},
"error.invalid-email": {
"defaultMessage": "无效的邮箱地址"
},
"error.max-character-length": {
"defaultMessage": "最大长度为 {max} 个字符"
},
"error.max-domains": {
"defaultMessage": "域名过多,最多为 {max} 个"
},
"error.maximum": {
"defaultMessage": "最大值为 {max}"
},
"error.min-character-length": {
"defaultMessage": "最小长度为 {min} 个字符"
},
"error.minimum": {
"defaultMessage": "最小值为 {min}"
},
"error.passwords-must-match": {
"defaultMessage": "密码必须匹配"
},
"error.required": {
"defaultMessage": "此项为必填项"
},
"expires.on": {
"defaultMessage": "过期时间: {date}"
},
"footer.github-fork": {
"defaultMessage": "在 Github 上复刻 (Fork) 本项目"
},
"host.flags.block-exploits": {
"defaultMessage": "阻止常见攻击"
},
"host.flags.cache-assets": {
"defaultMessage": "缓存资源"
},
"host.flags.preserve-path": {
"defaultMessage": "保留路径"
},
"host.flags.protocols": {
"defaultMessage": "协议"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets 支持"
},
"host.forward-port": {
"defaultMessage": "转发端口"
},
"host.forward-scheme": {
"defaultMessage": "协议"
},
"hosts": {
"defaultMessage": "主机列表"
},
"http-only": {
"defaultMessage": "仅 HTTP"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt DNS 验证"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt HTTP 验证"
},
"loading": {
"defaultMessage": "加载中···"
},
"login.title": {
"defaultMessage": "登录您的账户"
},
"nginx-config.label": {
"defaultMessage": "自定义 Nginx 配置"
},
"nginx-config.placeholder": {
"defaultMessage": "# 在此输入您的自定义 Nginx 配置,风险自负!"
},
"no-permission-error": {
"defaultMessage": "您无权查看此内容。"
},
"notfound.action": {
"defaultMessage": "返回首页"
},
"notfound.content": {
"defaultMessage": "很抱歉,您要查找的页面未找到"
},
"notfound.title": {
"defaultMessage": "糟糕...您刚刚找到了一个错误页面"
},
"notification.error": {
"defaultMessage": "错误"
},
"notification.object-deleted": {
"defaultMessage": "{object} 已被删除"
},
"notification.object-disabled": {
"defaultMessage": "{object} 已被禁用"
},
"notification.object-enabled": {
"defaultMessage": "{object} 已被启用"
},
"notification.object-renewed": {
"defaultMessage": "{object} 已续期"
},
"notification.object-saved": {
"defaultMessage": "{object} 已保存"
},
"notification.success": {
"defaultMessage": "成功"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "添加 {object}"
},
"object.delete": {
"defaultMessage": "删除 {object}"
},
"object.delete.content": {
"defaultMessage": "您确定要删除 {object} 吗?"
},
"object.edit": {
"defaultMessage": "编辑 {object}"
},
"object.empty": {
"defaultMessage": "没有 {objects}"
},
"object.event.created": {
"defaultMessage": "已创建 {object}"
},
"object.event.deleted": {
"defaultMessage": "已删除 {object}"
},
"object.event.disabled": {
"defaultMessage": "已禁用 {object}"
},
"object.event.enabled": {
"defaultMessage": "已启用 {object}"
},
"object.event.renewed": {
"defaultMessage": "已续期 {object}"
},
"object.event.updated": {
"defaultMessage": "已更新 {object}"
},
"offline": {
"defaultMessage": "离线"
},
"online": {
"defaultMessage": "在线"
},
"options": {
"defaultMessage": "选项"
},
"password": {
"defaultMessage": "密码"
},
"password.generate": {
"defaultMessage": "生成随机密码"
},
"password.hide": {
"defaultMessage": "隐藏密码"
},
"password.show": {
"defaultMessage": "显示密码"
},
"permissions.hidden": {
"defaultMessage": "隐藏"
},
"permissions.manage": {
"defaultMessage": "管理"
},
"permissions.view": {
"defaultMessage": "仅查看"
},
"permissions.visibility.all": {
"defaultMessage": "所有项目"
},
"permissions.visibility.title": {
"defaultMessage": "项目可见性"
},
"permissions.visibility.user": {
"defaultMessage": "仅创建的项目"
},
"proxy-host": {
"defaultMessage": "代理服务"
},
"proxy-host.forward-host": {
"defaultMessage": "转发主机名 / IP"
},
"proxy-hosts": {
"defaultMessage": "代理服务列表"
},
"proxy-hosts.count": {
"defaultMessage": "{count} 个代理服务"
},
"public": {
"defaultMessage": "公开"
},
"redirection-host": {
"defaultMessage": "重定向主机"
},
"redirection-host.forward-domain": {
"defaultMessage": "转发域名"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP 状态码"
},
"redirection-hosts": {
"defaultMessage": "重定向主机列表"
},
"redirection-hosts.count": {
"defaultMessage": "{count} 个重定向主机"
},
"role.admin": {
"defaultMessage": "管理员"
},
"role.standard-user": {
"defaultMessage": "标准用户"
},
"save": {
"defaultMessage": "保存"
},
"setting": {
"defaultMessage": "设置"
},
"settings": {
"defaultMessage": "设置列表"
},
"settings.default-site": {
"defaultMessage": "默认站点"
},
"settings.default-site.404": {
"defaultMessage": "错误页面"
},
"settings.default-site.444": {
"defaultMessage": "无响应 (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "欢迎页面"
},
"settings.default-site.description": {
"defaultMessage": "当 Nginx 遇到未知主机时显示什么"
},
"settings.default-site.html": {
"defaultMessage": "自定义 HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": ""
},
"settings.default-site.redirect": {
"defaultMessage": "重定向"
},
"setup.preamble": {
"defaultMessage": "通过创建您的管理员账户开始使用。"
},
"setup.title": {
"defaultMessage": "欢迎!"
},
"sign-in": {
"defaultMessage": "登录"
},
"ssl-certificate": {
"defaultMessage": "SSL 证书"
},
"stream": {
"defaultMessage": "端口转发"
},
"stream.forward-host": {
"defaultMessage": "转发主机"
},
"stream.incoming-port": {
"defaultMessage": "入站端口"
},
"streams": {
"defaultMessage": "端口转发列表"
},
"streams.count": {
"defaultMessage": "{count} 个端口转发"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "测试"
},
"user": {
"defaultMessage": "用户"
},
"user.change-password": {
"defaultMessage": "修改密码"
},
"user.confirm-password": {
"defaultMessage": "确认密码"
},
"user.current-password": {
"defaultMessage": "当前密码"
},
"user.edit-profile": {
"defaultMessage": "编辑资料"
},
"user.full-name": {
"defaultMessage": "全名"
},
"user.login-as": {
"defaultMessage": "登录用户 {name}"
},
"user.logout": {
"defaultMessage": "退出登录"
},
"user.new-password": {
"defaultMessage": "新密码"
},
"user.nickname": {
"defaultMessage": "昵称"
},
"user.set-password": {
"defaultMessage": "设置密码"
},
"user.set-permissions": {
"defaultMessage": "为用户 {name} 设置权限"
},
"user.switch-dark": {
"defaultMessage": "切换到深色模式"
},
"user.switch-light": {
"defaultMessage": "切换到浅色模式"
},
"username": {
"defaultMessage": "用户名"
},
"users": {
"defaultMessage": "用户列表"
}
}
================================================
FILE: frontend/src/main.tsx
================================================
import React from "react";
import ReactDOM from "react-dom/client";
import App from "src/App.tsx";
import "@tabler/core/dist/css/tabler.min.css";
import "@tabler/core/dist/js/tabler.min.js";
import "./App.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
,
);
================================================
FILE: frontend/src/modals/AccessListModal.tsx
================================================
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import type { AccessList, AccessListClient, AccessListItem } from "src/api/backend";
import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/components";
import { useAccessList, useSetAccessList } from "src/hooks";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showAccessListModal = (id: number | "new") => {
EasyModal.show(AccessListModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
}
const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAccessList(id, ["items", "clients"]);
const { mutate: setAccessList } = useSetAccessList();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = (values: any): string | null => {
// either Auths or Clients must be defined
if (values.items?.length === 0 && values.clients?.length === 0) {
return intl.formatMessage({ id: "error.access.at-least-one" });
}
// ensure the items don't contain the same username twice
const usernames = values.items.map((i: any) => i.username);
const uniqueUsernames = Array.from(new Set(usernames));
if (usernames.length !== uniqueUsernames.length) {
return intl.formatMessage({ id: "error.access.duplicate-usernames" });
}
return null;
};
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
const vErr = validate(values);
if (vErr) {
setErrorMsg(vErr);
return;
}
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
...values,
};
// Filter out "items" to only use the "username" and "password" fields
payload.items = (values.items || []).map((i: AccessListItem) => ({
username: i.username,
password: i.password,
}));
// Filter out "clients" to only use the "directive" and "address" fields
payload.clients = (values.clients || []).map((i: AccessListClient) => ({
directive: i.directive,
address: i.address,
}));
setAccessList(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("access-list", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
const toggleClasses = "form-check-input";
const toggleEnabled = cn(toggleClasses, "bg-cyan");
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
{({ setFieldValue }: any) => (
)}
)}
);
});
export { showAccessListModal };
================================================
FILE: frontend/src/modals/ChangePasswordModal.tsx
================================================
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { updateAuth } from "src/api/backend";
import { Button } from "src/components";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
const showChangePasswordModal = (id: number | "me") => {
EasyModal.show(ChangePasswordModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me";
}
const ChangePasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (values.new !== values.confirm) {
setError( );
setSubmitting(false);
return;
}
if (isSubmitting) return;
setIsSubmitting(true);
setError(null);
try {
await updateAuth(id, values.new, values.current);
remove();
} catch (err: any) {
setError( );
}
setIsSubmitting(false);
setSubmitting(false);
};
return (
{() => (
)}
);
});
export { showChangePasswordModal };
================================================
FILE: frontend/src/modals/CustomCertificateModal.tsx
================================================
import { IconAlertTriangle } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend";
import { Button } from "src/components";
import { T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showCustomCertificateModal = () => {
EasyModal.show(CustomCertificateModal);
};
const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
const { niceName, provider, certificate, certificateKey, intermediateCertificate } = values;
const formData = new FormData();
formData.append("certificate", certificate);
formData.append("certificate_key", certificateKey);
if (intermediateCertificate !== null) {
formData.append("intermediate_certificate", intermediateCertificate);
}
// Validate
await validateCertificate(formData);
// Create certificate, as other without anything else
const cert = await createCertificate({ niceName, provider } as Certificate);
// Upload the certificates to the created certificate
await uploadCertificate(cert.id, formData);
// Success
showObjectSuccess("certificate", "saved");
remove();
} catch (err: any) {
setErrorMsg( );
}
queryClient.invalidateQueries({ queryKey: ["certificates"] });
setIsSubmitting(false);
setSubmitting(false);
};
return (
{() => (
)}
);
});
export { showCustomCertificateModal };
================================================
FILE: frontend/src/modals/DNSCertificateModal.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik, Field } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { createCertificate } from "src/api/backend";
import { Button, DNSProviderFields, DomainNamesField } from "src/components";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showDNSCertificateModal = () => {
EasyModal.show(DNSCertificateModal);
};
const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
await createCertificate(values);
showObjectSuccess("certificate", "saved");
remove();
} catch (err: any) {
setErrorMsg( );
}
queryClient.invalidateQueries({ queryKey: ["certificates"] });
setIsSubmitting(false);
setSubmitting(false);
};
return (
{() => (
)}
);
});
export { showDNSCertificateModal };
================================================
FILE: frontend/src/modals/DeadHostModal.tsx
================================================
import { IconSettings } from "@tabler/icons-react";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import {
Button,
DomainNamesField,
Loading,
NginxConfigField,
SSLCertificateField,
SSLOptionsFields,
} from "src/components";
import { useDeadHost, useSetDeadHost } from "src/hooks";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showDeadHostModal = (id: number | "new") => {
EasyModal.show(DeadHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
}
const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useDeadHost(id);
const { mutate: setDeadHost } = useSetDeadHost();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
...values,
};
setDeadHost(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("dead-host", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
{() => (
)}
)}
);
});
export { showDeadHostModal };
================================================
FILE: frontend/src/modals/DeleteConfirmModal.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button } from "src/components";
import { T } from "src/locale";
interface ShowProps {
title?: ReactNode;
tTitle?: string;
children: ReactNode;
onConfirm: () => Promise | void;
invalidations?: any[];
}
interface Props extends InnerModalProps, ShowProps {}
const showDeleteConfirmModal = (props: ShowProps) => {
EasyModal.show(DeleteConfirmModal, props);
};
const DeleteConfirmModal = EasyModal.create(
({ title, tTitle, children, onConfirm, invalidations, visible, remove }: Props) => {
const queryClient = useQueryClient();
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
setError(null);
try {
await onConfirm();
remove();
// invalidate caches as requested
invalidations?.forEach((inv) => {
queryClient.invalidateQueries({ queryKey: inv });
});
} catch (err: any) {
setError( );
}
setIsSubmitting(false);
};
return (
{tTitle ? : title ? title : null}
setError(null)} dismissible>
{error}
{children}
);
},
);
export { showDeleteConfirmModal };
================================================
FILE: frontend/src/modals/EventDetailsModal.tsx
================================================
import CodeEditor from "@uiw/react-textarea-code-editor";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components";
import { useAuditLog } from "src/hooks";
import { T } from "src/locale";
const showEventDetailsModal = (id: number) => {
EasyModal.show(EventDetailsModal, { id });
};
interface Props extends InnerModalProps {
id: number;
}
const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAuditLog(id);
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
<>
>
)}
);
});
export { showEventDetailsModal };
================================================
FILE: frontend/src/modals/HTTPCertificateModal.tsx
================================================
import { IconAlertTriangle } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik, Field } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { createCertificate, testHttpCertificate } from "src/api/backend";
import { Button, DomainNamesField } from "src/components";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
const showHTTPCertificateModal = () => {
EasyModal.show(HTTPCertificateModal);
};
const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [domains, setDomains] = useState([] as string[]);
const [isTesting, setIsTesting] = useState(false);
const [testResults, setTestResults] = useState(null as Record | null);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
await createCertificate(values);
showObjectSuccess("certificate", "saved");
remove();
} catch (err: any) {
setErrorMsg( );
}
queryClient.invalidateQueries({ queryKey: ["certificates"] });
setIsSubmitting(false);
setSubmitting(false);
};
const handleTest = async () => {
setIsTesting(true);
setErrorMsg(null);
setTestResults(null);
try {
const result = await testHttpCertificate(domains);
setTestResults(result);
} catch (err: any) {
setErrorMsg( );
}
setIsTesting(false);
};
const parseTestResults = () => {
const elms = [];
for (const domain in testResults) {
const status = testResults[domain];
if (status === "ok") {
elms.push(
{domain}:
,
);
} else {
if (status === "no-host") {
elms.push(
{domain}:
,
);
} else if (status === "failed") {
elms.push(
{domain}:
,
);
} else if (status === "404") {
elms.push(
{domain}:
,
);
} else if (status === "wrong-data") {
elms.push(
{domain}:
,
);
} else if (status.startsWith("other:")) {
const code = status.substring(6);
elms.push(
{domain}:
,
);
} else {
// This should never happen
elms.push(
{domain}: ?
,
);
}
}
}
return <>{elms}>;
};
return (
{() => (
)}
);
});
export { showHTTPCertificateModal };
================================================
FILE: frontend/src/modals/HelpModal.tsx
================================================
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { useEffect, useState } from "react";
import Modal from "react-bootstrap/Modal";
import ReactMarkdown from "react-markdown";
import { Button } from "src/components";
import { getLocale, T } from "src/locale";
import { getHelpFile } from "src/locale/src/HelpDoc";
interface Props extends InnerModalProps {
section: string;
color?: string;
}
const showHelpModal = (section: string, color?: string) => {
EasyModal.show(HelpModal, { section, color });
};
const HelpModal = EasyModal.create(({ section, color, visible, remove }: Props) => {
const [markdownText, setMarkdownText] = useState("");
const lang = getLocale(true);
useEffect(() => {
try {
const docFile = getHelpFile(lang, section) as any;
fetch(docFile)
.then((response) => response.text())
.then(setMarkdownText);
} catch (ex: any) {
setMarkdownText(`**ERROR:** ${ex.message}`);
}
}, [lang, section]);
return (
{markdownText}
);
});
export { showHelpModal };
================================================
FILE: frontend/src/modals/PermissionsModal.module.css
================================================
.active {
border-color: var(--tblr-orange) !important;
}
================================================
FILE: frontend/src/modals/PermissionsModal.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { setPermissions } from "src/api/backend";
import { Button, Loading } from "src/components";
import { useUser } from "src/hooks";
import { T } from "src/locale";
import styles from "./PermissionsModal.module.css";
const showPermissionsModal = (id: number) => {
EasyModal.show(PermissionsModal, { id });
};
interface Props extends InnerModalProps {
id: number;
}
const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState(null);
const { data, isLoading, error } = useUser(id);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
try {
await setPermissions(id, values);
remove();
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["user"] });
} catch (err: any) {
setErrorMsg( );
}
setSubmitting(false);
setIsSubmitting(false);
};
const getClasses = (active: boolean) => {
return cn("btn", active ? styles.active : null, {
active,
"bg-orange-lt": active,
});
};
// given the field and clicked permission, intelligently set the value, and
// other values that depends on it.
const handleChange = (form: any, field: any, perm: string) => {
if (field.name === "proxyHosts" && perm !== "hidden" && form.values.accessLists === "hidden") {
form.setFieldValue("accessLists", "view");
}
// certs are required for proxy and redirection hosts, and streams
if (
["proxyHosts", "redirectionHosts", "deadHosts", "streams"].includes(field.name) &&
perm !== "hidden" &&
form.values.certificates === "hidden"
) {
form.setFieldValue("certificates", "view");
}
form.setFieldValue(field.name, perm);
};
const getPermissionButtons = (field: any, form: any) => {
const isManage = field.value === "manage";
const isView = field.value === "view";
const isHidden = field.value === "hidden";
let hiddenDisabled = false;
if (field.name === "accessLists") {
hiddenDisabled = form.values.proxyHosts !== "hidden";
}
if (field.name === "certificates") {
hiddenDisabled =
form.values.proxyHosts !== "hidden" ||
form.values.redirectionHosts !== "hidden" ||
form.values.deadHosts !== "hidden" ||
form.values.streams !== "hidden";
}
return (
);
};
const isAdmin = data?.roles.indexOf("admin") !== -1;
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
{() => (
)}
)}
);
});
export { showPermissionsModal };
================================================
FILE: frontend/src/modals/ProxyHostModal.tsx
================================================
import { IconSettings } from "@tabler/icons-react";
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import {
AccessField,
Button,
DomainNamesField,
HasPermission,
Loading,
LocationsFields,
NginxConfigField,
SSLCertificateField,
SSLOptionsFields,
} from "src/components";
import { useProxyHost, useSetProxyHost, useUser } from "src/hooks";
import { T } from "src/locale";
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
import { validateNumber, validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showProxyHostModal = (id: number | "new") => {
EasyModal.show(ProxyHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
}
const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data: currentUser, isLoading: userIsLoading, error: userError } = useUser("me");
const { data, isLoading, error } = useProxyHost(id);
const { mutate: setProxyHost } = useSetProxyHost();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
...values,
};
setProxyHost(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("proxy-host", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
{!isLoading && (error || userError) && (
{error?.message || userError?.message || "Unknown error"}
)}
{isLoading || (userIsLoading && )}
{!isLoading && !userIsLoading && data && currentUser && (
{() => (
)}
)}
);
});
export { showProxyHostModal };
================================================
FILE: frontend/src/modals/RedirectionHostModal.tsx
================================================
import { IconSettings } from "@tabler/icons-react";
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import {
Button,
DomainNamesField,
Loading,
NginxConfigField,
SSLCertificateField,
SSLOptionsFields,
} from "src/components";
import { useRedirectionHost, useSetRedirectionHost } from "src/hooks";
import { T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showRedirectionHostModal = (id: number | "new") => {
EasyModal.show(RedirectionHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
}
const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useRedirectionHost(id);
const { mutate: setRedirectionHost } = useSetRedirectionHost();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
...values,
};
setRedirectionHost(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("redirection-host", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
{() => (
)}
)}
);
});
export { showRedirectionHostModal };
================================================
FILE: frontend/src/modals/RenewCertificateModal.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { type ReactNode, useEffect, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { renewCertificate } from "src/api/backend";
import { Button, Loading } from "src/components";
import { useCertificate } from "src/hooks";
import { T } from "src/locale";
import { showObjectSuccess } from "src/notifications";
interface Props extends InnerModalProps {
id: number;
}
const showRenewCertificateModal = (id: number) => {
EasyModal.show(RenewCertificateModal, { id });
};
const RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props) => {
const queryClient = useQueryClient();
const { data, isLoading, error } = useCertificate(id);
const [errorMsg, setErrorMsg] = useState(null);
const [isFresh, setIsFresh] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!data || !isFresh || isSubmitting) return;
setIsFresh(false);
setIsSubmitting(true);
renewCertificate(id)
.then(() => {
showObjectSuccess("certificate", "renewed");
queryClient.invalidateQueries({ queryKey: ["certificates"] });
remove();
})
.catch((err: any) => {
setErrorMsg( );
})
.finally(() => {
setIsSubmitting(false);
});
}, [id, data, isFresh, isSubmitting, remove, queryClient]);
return (
{errorMsg}
{isLoading && }
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{data && isSubmitting && !errorMsg ? Please wait ...
: null}
);
});
export { showRenewCertificateModal };
================================================
FILE: frontend/src/modals/SetPasswordModal.tsx
================================================
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { generate } from "generate-password-browser";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { updateAuth } from "src/api/backend";
import { Button } from "src/components";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
const showSetPasswordModal = (id: number) => {
EasyModal.show(SetPasswordModal, { id });
};
interface Props extends InnerModalProps {
id: number;
}
const SetPasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState(null);
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setError(null);
try {
await updateAuth(id, values.new);
remove();
} catch (err: any) {
setError( );
}
setIsSubmitting(false);
setSubmitting(false);
};
return (
{() => (
)}
);
});
export { showSetPasswordModal };
================================================
FILE: frontend/src/modals/StreamModal.tsx
================================================
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button, Loading, SSLCertificateField, SSLOptionsFields } from "src/components";
import { useSetStream, useStream } from "src/hooks";
import { intl, T } from "src/locale";
import { validateNumber, validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showStreamModal = (id: number | "new") => {
EasyModal.show(StreamModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
}
const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useStream(id);
const { mutate: setStream } = useSetStream();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
...values,
};
setStream(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("stream", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{isLoading && }
{!isLoading && data && (
{({ setFieldValue }: any) => (
)}
)}
);
});
export { showStreamModal };
================================================
FILE: frontend/src/modals/TwoFactorModal.tsx
================================================
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import {
disable2FA,
enable2FA,
get2FAStatus,
regenerateBackupCodes,
start2FASetup,
} from "src/api/backend";
import { Button } from "src/components";
import { T } from "src/locale";
import { validateString } from "src/modules/Validations";
type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disable";
const showTwoFactorModal = (id: number | "me") => {
EasyModal.show(TwoFactorModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me";
}
const TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState(null);
const [step, setStep] = useState("loading");
const [isEnabled, setIsEnabled] = useState(false);
const [backupCodesRemaining, setBackupCodesRemaining] = useState(0);
const [setupData, setSetupData] = useState<{ secret: string; otpauthUrl: string } | null>(null);
const [backupCodes, setBackupCodes] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const loadStatus = useCallback(async () => {
try {
const status = await get2FAStatus(id);
setIsEnabled(status.enabled);
setBackupCodesRemaining(status.backupCodesRemaining);
setStep("status");
} catch (err: any) {
setError(err.message || "Failed to load 2FA status");
setStep("status");
}
}, [id]);
useEffect(() => {
loadStatus();
}, [loadStatus]);
const handleStartSetup = async () => {
setError(null);
setIsSubmitting(true);
try {
const data = await start2FASetup(id);
setSetupData(data);
setStep("setup");
} catch (err: any) {
setError(err.message || "Failed to start 2FA setup");
}
setIsSubmitting(false);
};
const handleVerify = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
const result = await enable2FA(id, values.code);
setBackupCodes(result.backupCodes);
setStep("backup");
} catch (err: any) {
setError(err.message || "Failed to enable 2FA");
}
setIsSubmitting(false);
};
const handleDisable = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
await disable2FA(id, values.code);
setIsEnabled(false);
setStep("status");
} catch (err: any) {
setError(err.message || "Failed to disable 2FA");
}
setIsSubmitting(false);
};
const handleRegenerateBackup = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
const result = await regenerateBackupCodes(id, values.code);
setBackupCodes(result.backupCodes);
setStep("backup");
} catch (err: any) {
setError(err.message || "Failed to regenerate backup codes");
}
setIsSubmitting(false);
};
const handleBackupDone = () => {
setIsEnabled(true);
setBackupCodes([]);
loadStatus();
};
const renderContent = () => {
if (step === "loading") {
return (
);
}
if (step === "status") {
return (
{isEnabled ? : }
{isEnabled && (
)}
{!isEnabled ? (
) : (
setStep("disable")}>
setStep("verify")}>
)}
);
}
if (step === "setup" && setupData) {
return (
);
}
if (step === "backup") {
return (
{backupCodes.map((code, index) => (
{code}
))}
);
}
if (step === "disable") {
return (
);
}
if (step === "verify") {
return (
);
}
return null;
};
return (
setError(null)} dismissible>
{error}
{renderContent()}
);
});
export { showTwoFactorModal };
================================================
FILE: frontend/src/modals/UserModal.tsx
================================================
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button, Loading } from "src/components";
import { useSetUser, useUser } from "src/hooks";
import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
const showUserModal = (id: number | "me" | "new") => {
EasyModal.show(UserModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me" | "new";
}
const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useUser(id);
const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
const { mutate: setUser } = useSetUser();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const { ...payload } = {
id: id === "new" ? undefined : id,
roles: [],
...values,
};
if (data?.id === currentUser?.id) {
// Prevent user from locking themselves out
delete payload.isDisabled;
delete payload.roles;
} else if (payload.isAdmin) {
payload.roles = ["admin"];
}
// this isn't a real field, just for the form
delete payload.isAdmin;
setUser(payload, {
onError: (err: any) => setErrorMsg(err.message),
onSuccess: () => {
showObjectSuccess("user", "saved");
remove();
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
return (
{!isLoading && error && (
{error?.message || "Unknown error"}
)}
{(isLoading || currentIsLoading) && }
{!isLoading && !currentIsLoading && data && currentUser && (
{() => (
)}
)}
);
});
export { showUserModal };
================================================
FILE: frontend/src/modals/index.ts
================================================
export * from "./AccessListModal";
export * from "./ChangePasswordModal";
export * from "./CustomCertificateModal";
export * from "./DeadHostModal";
export * from "./DeleteConfirmModal";
export * from "./DNSCertificateModal";
export * from "./EventDetailsModal";
export * from "./HelpModal";
export * from "./HTTPCertificateModal";
export * from "./PermissionsModal";
export * from "./ProxyHostModal";
export * from "./RedirectionHostModal";
export * from "./RenewCertificateModal";
export * from "./SetPasswordModal";
export * from "./StreamModal";
export * from "./TwoFactorModal";
export * from "./UserModal";
================================================
FILE: frontend/src/modules/AuthStore.ts
================================================
import { getUnixTime, parseISO } from "date-fns";
import type { TokenResponse } from "src/api/backend";
export const TOKEN_KEY = "authentications";
export class AuthStore {
// Get all tokens from stack
get tokens() {
const t = localStorage.getItem(TOKEN_KEY);
let tokens = [];
if (t !== null) {
try {
tokens = JSON.parse(t);
} catch (e) {
console.error("Failed to parse tokens from localStorage", e);
}
}
return tokens;
}
// Get last token from stack
get token() {
const t = this.tokens;
if (t.length) {
return t[t.length - 1];
}
return null;
}
// Get expires from last token
get expires() {
const t = this.token;
if (t && typeof t.expires !== "undefined") {
const expires = Number(t.expires);
if (expires && !Number.isNaN(expires)) {
return expires;
}
}
return null;
}
// Filter out invalid tokens and return true if we find one that is valid
// hasActiveToken() {
// const t = this.tokens;
// return t.length > 0;
// }
// Start from the END of the stack and work backwards
hasActiveToken() {
const t = this.tokens;
if (!t.length) {
return false;
}
const now = Math.round(Date.now() / 1000);
const oneMinuteBuffer = 60;
for (let i = t.length - 1; i >= 0; i--) {
const dte = getUnixTime(parseISO(t[i].expires));
const valid = dte - oneMinuteBuffer > now;
if (valid) {
return true;
}
this.drop();
}
return false;
}
// Set a single token on the stack
set({ token, expires }: TokenResponse) {
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
}
// Add a token to the END of the stack
add({ token, expires }: TokenResponse) {
const t = this.tokens;
t.push({ token, expires });
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
}
// Drop a token from the END of the stack
drop() {
const t = this.tokens;
t.splice(-1, 1);
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
}
clear() {
localStorage.removeItem(TOKEN_KEY);
}
count() {
return this.tokens.length;
}
}
export default new AuthStore();
================================================
FILE: frontend/src/modules/Permissions.ts
================================================
import type { UserPermissions } from "src/api/backend";
export const ADMIN = "admin";
export const VISIBILITY = "visibility";
export const PROXY_HOSTS = "proxyHosts";
export const REDIRECTION_HOSTS = "redirectionHosts";
export const DEAD_HOSTS = "deadHosts";
export const STREAMS = "streams";
export const CERTIFICATES = "certificates";
export const ACCESS_LISTS = "accessLists";
export const MANAGE = "manage";
export const VIEW = "view";
export const HIDDEN = "hidden";
export const ALL = "all";
export const USER = "user";
export type Section =
| typeof ADMIN
| typeof VISIBILITY
| typeof PROXY_HOSTS
| typeof REDIRECTION_HOSTS
| typeof DEAD_HOSTS
| typeof STREAMS
| typeof CERTIFICATES
| typeof ACCESS_LISTS;
export type Permission = typeof MANAGE | typeof VIEW;
const hasPermission = (
section: Section,
perm: Permission,
userPerms: UserPermissions | undefined,
roles: string[] | undefined,
): boolean => {
if (!userPerms) return false;
if (isAdmin(roles)) return true;
const acceptable = [MANAGE, perm];
// @ts-expect-error 7053
const v = typeof userPerms[section] !== "undefined" ? userPerms[section] : HIDDEN;
return acceptable.indexOf(v) !== -1;
};
const isAdmin = (roles: string[] | undefined): boolean => {
return roles?.includes("admin") || false;
};
export { hasPermission, isAdmin };
================================================
FILE: frontend/src/modules/Validations.tsx
================================================
import { intl } from "src/locale";
const validateString = (minLength = 0, maxLength = 0) => {
if (minLength <= 0 && maxLength <= 0) {
// this doesn't require translation
console.error("validateString() must be called with a min or max or both values in order to work!");
}
return (value: string): string | undefined => {
if (minLength && (typeof value === "undefined" || !value.length)) {
return intl.formatMessage({ id: "error.required" });
}
if (minLength && value.length < minLength) {
return intl.formatMessage({ id: "error.min-character-length" }, { min: minLength });
}
if (maxLength && (typeof value === "undefined" || value.length > maxLength)) {
return intl.formatMessage({ id: "error.max-character-length" }, { max: maxLength });
}
};
};
const validateNumber = (min = -1, max = -1) => {
if (min === -1 && max === -1) {
// this doesn't require translation
console.error("validateNumber() must be called with a min or max or both values in order to work!");
}
return (value: string): string | undefined => {
const int: number = +value;
if (min > -1 && !int) {
return intl.formatMessage({ id: "error.required" });
}
if (min > -1 && int < min) {
return intl.formatMessage({ id: "error.minimum" }, { min });
}
if (max > -1 && int > max) {
return intl.formatMessage({ id: "error.maximum" }, { max });
}
};
};
const validateEmail = () => {
return (value: string): string | undefined => {
if (!value.length) {
return intl.formatMessage({ id: "error.required" });
}
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i.test(value)) {
return intl.formatMessage({ id: "error.invalid-email" });
}
};
};
const validateDomain = (allowWildcards = false) => {
return (d: string): boolean => {
const dom = d.trim().toLowerCase();
if (dom.length < 3) {
return false;
}
// Prevent wildcards
if (!allowWildcards && dom.indexOf("*") !== -1) {
return false;
}
// Prevent duplicate * in domain
if ((dom.match(/\*/g) || []).length > 1) {
return false;
}
// Prevent some invalid characters
if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) {
return false;
}
// This will match *.com type domains,
return dom.match(/\*\.[^.]+$/m) === null;
};
};
const validateDomains = (allowWildcards = false, maxDomains?: number) => {
const vDom = validateDomain(allowWildcards);
return (value?: string[]): string | undefined => {
if (!value?.length) {
return intl.formatMessage({ id: "error.required" });
}
// Deny if the list of domains is hit
if (maxDomains && value?.length >= maxDomains) {
return intl.formatMessage({ id: "error.max-domains" }, { max: maxDomains });
}
// validate each domain
for (let i = 0; i < value?.length; i++) {
if (!vDom(value[i])) {
return intl.formatMessage({ id: "error.invalid-domain" }, { domain: value[i] });
}
}
};
};
export { validateEmail, validateNumber, validateString, validateDomains, validateDomain };
================================================
FILE: frontend/src/notifications/Msg.module.css
================================================
.toaster {
padding: 0;
background: transparent !important;
box-shadow: none !important;
border: none !important;
&.toast {
border-radius: 0;
box-shadow: none;
font-size: 14px;
padding: 16px 24px;
background: transparent;
}
}
================================================
FILE: frontend/src/notifications/Msg.tsx
================================================
import { IconCheck, IconExclamationCircle } from "@tabler/icons-react";
import cn from "classnames";
import type { ReactNode } from "react";
function Msg({ data }: any) {
const cns = cn("toast", "show", data.type || null);
let icon: ReactNode = null;
switch (data.type) {
case "success":
icon = ;
break;
case "error":
icon = ;
break;
}
return (
{data.title && (
{icon} {data.title}
)}
{data.message}
);
}
export { Msg };
================================================
FILE: frontend/src/notifications/helpers.tsx
================================================
import { toast } from "react-toastify";
import { intl } from "src/locale";
import { Msg } from "./Msg";
import styles from "./Msg.module.css";
const showSuccess = (message: string) => {
toast(Msg, {
className: styles.toaster,
data: {
type: "success",
title: intl.formatMessage({ id: "notification.success" }),
message,
},
});
};
const showError = (message: string) => {
toast( , {
data: {
type: "error",
title: intl.formatMessage({ id: "notification.error" }),
message,
},
});
};
const showObjectSuccess = (obj: string, action: string) => {
showSuccess(
intl.formatMessage(
{
id: `notification.object-${action}`,
},
{ object: intl.formatMessage({ id: obj }) },
),
);
};
export { showSuccess, showError, showObjectSuccess };
================================================
FILE: frontend/src/notifications/index.ts
================================================
export * from "./helpers";
================================================
FILE: frontend/src/pages/Access/Table.tsx
================================================
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { AccessList } from "src/api/backend";
import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
interface Props {
data: AccessList[];
isFiltered?: boolean;
isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
onNew?: () => void;
}
export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onNew }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "name",
header: intl.formatMessage({ id: "column.name" }),
cell: (info: any) => (
),
}),
columnHelper.accessor((row: any) => row.items, {
id: "items",
header: intl.formatMessage({ id: "column.authorization" }),
cell: (info: any) => ,
}),
columnHelper.accessor((row: any) => row.clients, {
id: "clients",
header: intl.formatMessage({ id: "column.access" }),
cell: (info: any) => ,
}),
columnHelper.accessor((row: any) => row.satisfyAny, {
id: "satisfyAny",
header: intl.formatMessage({ id: "column.satisfy" }),
cell: (info: any) => ,
}),
columnHelper.accessor((row: any) => row.proxyHostCount, {
id: "proxyHostCount",
header: intl.formatMessage({ id: "proxy-hosts" }),
cell: (info: any) => ,
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onEdit, onDelete],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Access/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteAccessList } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useAccessLists } from "src/hooks";
import { T } from "src/locale";
import { showAccessListModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
import { showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteAccessList(id);
showObjectSuccess("access-list", "deleted");
};
let filtered = null;
if (search && data) {
filtered = data?.filter((item) => {
return item.name.toLowerCase().includes(search);
});
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("AccessLists", "cyan")}>
{data?.length ? (
showAccessListModal("new")}
>
) : null}
showAccessListModal(id)}
onDelete={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["access-lists"], ["access-list", id]],
children: ,
})
}
onNew={() => showAccessListModal("new")}
/>
);
}
================================================
FILE: frontend/src/pages/Access/index.tsx
================================================
import { HasPermission } from "src/components";
import { ACCESS_LISTS, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const Access = () => {
return (
);
};
export default Access;
================================================
FILE: frontend/src/pages/AuditLog/Table.tsx
================================================
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { AuditLog } from "src/api/backend";
import { EventFormatter, GravatarFormatter } from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
interface Props {
data: AuditLog[];
isFetching?: boolean;
onSelectItem?: (id: number) => void;
}
export default function Table({ data, isFetching, onSelectItem }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: AuditLog) => row.user, {
id: "user.avatar",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: AuditLog) => row, {
id: "objectType",
header: intl.formatMessage({ id: "column.event" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
{
e.preventDefault();
onSelectItem?.(info.row.original.id);
}}
>
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onSelectItem],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return ;
}
================================================
FILE: frontend/src/pages/AuditLog/TableWrapper.tsx
================================================
import Alert from "react-bootstrap/Alert";
import { LoadingPage } from "src/components";
import { useAuditLogs } from "src/hooks";
import { T } from "src/locale";
import { showEventDetailsModal } from "src/modals";
import Table from "./Table";
export default function TableWrapper() {
const { isFetching, isLoading, isError, error, data } = useAuditLogs(["user"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
return (
);
}
================================================
FILE: frontend/src/pages/AuditLog/index.tsx
================================================
import { HasPermission } from "src/components";
import { ADMIN, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const AuditLog = () => {
return (
);
};
export default AuditLog;
================================================
FILE: frontend/src/pages/Certificates/Table.tsx
================================================
import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { Certificate } from "src/api/backend";
import {
CertificateInUseFormatter,
DateFormatter,
DomainsFormatter,
EmptyData,
GravatarFormatter,
HasPermission,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
interface Props {
data: Certificate[];
isFiltered?: boolean;
isFetching?: boolean;
onDelete?: (id: number) => void;
onRenew?: (id: number) => void;
onDownload?: (id: number) => void;
}
export default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "domainNames",
header: intl.formatMessage({ id: "column.name" }),
cell: (info: any) => {
const value = info.getValue();
return (
);
},
}),
columnHelper.accessor((row: any) => row, {
id: "provider",
header: intl.formatMessage({ id: "column.provider" }),
cell: (info: any) => {
const r = info.getValue();
if (r.provider === "letsencrypt") {
if (r.meta?.dnsChallenge && r.meta?.dnsProvider) {
return (
<>
– {r.meta?.dnsProvider}
>
);
}
return ;
}
if (r.provider === "other") {
return ;
}
return ;
},
}),
columnHelper.accessor((row: any) => row.expiresOn, {
id: "expiresOn",
header: intl.formatMessage({ id: "column.expires" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row, {
id: "proxyHosts",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
const r = info.getValue();
return (
);
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onDelete, onRenew, onDownload],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
const customAddBtn = (
);
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Certificates/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteCertificate, downloadCertificate } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useCertificates } from "src/hooks";
import { T } from "src/locale";
import {
showCustomCertificateModal,
showDeleteConfirmModal,
showDNSCertificateModal,
showHelpModal,
showHTTPCertificateModal,
showRenewCertificateModal,
} from "src/modals";
import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
import { showError, showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useCertificates([
"owner",
"dead_hosts",
"proxy_hosts",
"redirection_hosts",
"streams",
]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteCertificate(id);
showObjectSuccess("certificate", "deleted");
};
const handleDownload = async (id: number) => {
try {
await downloadCertificate(id);
} catch (err: any) {
showError(err.message);
}
};
let filtered = null;
if (search && data) {
filtered = data?.filter(
(item) =>
item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
item.niceName.toLowerCase().includes(search),
);
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("Certificates", "pink")}>
{data?.length ? (
) : null}
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["certificates"], ["certificate", id]],
children: ,
})
}
/>
);
}
================================================
FILE: frontend/src/pages/Certificates/index.tsx
================================================
import { HasPermission } from "src/components";
import { CERTIFICATES, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const Certificates = () => {
return (
);
};
export default Certificates;
================================================
FILE: frontend/src/pages/Dashboard/index.tsx
================================================
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
import { useNavigate } from "react-router-dom";
import { HasPermission } from "src/components";
import { useHostReport } from "src/hooks";
import { T } from "src/locale";
import { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from "src/modules/Permissions";
const Dashboard = () => {
const { data: hostReport } = useHostReport();
const navigate = useNavigate();
return (
);
};
export default Dashboard;
================================================
FILE: frontend/src/pages/Login/index.module.css
================================================
.logo {
width: 200px;
}
================================================
FILE: frontend/src/pages/Login/index.tsx
================================================
import { Field, Form, Formik } from "formik";
import { useEffect, useRef, useState } from "react";
import Alert from "react-bootstrap/Alert";
import { Button, LocalePicker, Page, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context";
import { useHealth } from "src/hooks";
import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
import styles from "./index.module.css";
function TwoFactorForm() {
const codeRef = useRef(null);
const [formErr, setFormErr] = useState("");
const { verifyTwoFactor, cancelTwoFactor } = useAuthState();
const onSubmit = async (values: any, { setSubmitting }: any) => {
setFormErr("");
try {
await verifyTwoFactor(values.code);
} catch (err) {
if (err instanceof Error) {
setFormErr(err.message);
}
}
setSubmitting(false);
};
useEffect(() => {
codeRef.current?.focus();
}, []);
return (
<>
{formErr !== "" && {formErr} }
{({ isSubmitting }) => (
)}
>
);
}
function LoginForm() {
const emailRef = useRef(null);
const [formErr, setFormErr] = useState("");
const { login } = useAuthState();
const onSubmit = async (values: any, { setSubmitting }: any) => {
setFormErr("");
try {
await login(values.email, values.password);
} catch (err) {
if (err instanceof Error) {
setFormErr(err.message);
}
}
setSubmitting(false);
};
useEffect(() => {
emailRef.current?.focus();
}, []);
return (
<>
{formErr !== "" && {formErr} }
{({ isSubmitting }) => (
)}
>
);
}
export default function Login() {
const { twoFactorChallenge } = useAuthState();
const health = useHealth();
const getVersion = () => {
if (!health.data) {
return "";
}
const v = health.data.version;
return `v${v.major}.${v.minor}.${v.revision}`;
};
return (
{twoFactorChallenge ? : }
{getVersion()}
);
}
================================================
FILE: frontend/src/pages/Nginx/DeadHosts/Table.tsx
================================================
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { DeadHost } from "src/api/backend";
import {
CertificateFormatter,
DomainsFormatter,
EmptyData,
GravatarFormatter,
HasPermission,
TrueFalseFormatter,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
interface Props {
data: DeadHost[];
isFiltered?: boolean;
isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNew?: () => void;
}
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "domainNames",
header: intl.formatMessage({ id: "column.source" }),
cell: (info: any) => {
const value = info.getValue();
return ;
},
}),
columnHelper.accessor((row: any) => row.certificate, {
id: "certificate",
header: intl.formatMessage({ id: "column.ssl" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.enabled, {
id: "enabled",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onDelete, onEdit, onDisableToggle],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useDeadHosts } from "src/hooks";
import { T } from "src/locale";
import { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
import { showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteDeadHost(id);
showObjectSuccess("dead-host", "deleted");
};
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleDeadHost(id, enabled);
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
showObjectSuccess("dead-host", enabled ? "enabled" : "disabled");
};
let filtered = null;
if (search && data) {
filtered = data?.filter((item) => {
return item.domainNames.some((domain: string) => domain.toLowerCase().includes(search));
});
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("DeadHosts", "red")}>
{data?.length ? (
showDeadHostModal("new")}>
) : null}
showDeadHostModal(id)}
onDelete={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["dead-hosts"], ["dead-host", id]],
children: ,
})
}
onDisableToggle={handleDisableToggle}
onNew={() => showDeadHostModal("new")}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/DeadHosts/index.tsx
================================================
import { HasPermission } from "src/components";
import { DEAD_HOSTS, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const DeadHosts = () => {
return (
);
};
export default DeadHosts;
================================================
FILE: frontend/src/pages/Nginx/ProxyHosts/Table.tsx
================================================
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { ProxyHost } from "src/api/backend";
import {
AccessListFormatter,
CertificateFormatter,
DomainsFormatter,
EmptyData,
GravatarFormatter,
HasPermission,
TrueFalseFormatter,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
interface Props {
data: ProxyHost[];
isFiltered?: boolean;
isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNew?: () => void;
}
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "domainNames",
header: intl.formatMessage({ id: "column.source" }),
cell: (info: any) => {
const value = info.getValue();
return ;
},
}),
columnHelper.accessor((row: any) => row, {
id: "forwardHost",
header: intl.formatMessage({ id: "column.destination" }),
cell: (info: any) => {
const value = info.getValue();
return `${value.forwardScheme}://${value.forwardHost}:${value.forwardPort}`;
},
}),
columnHelper.accessor((row: any) => row.certificate, {
id: "certificate",
header: intl.formatMessage({ id: "column.ssl" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.accessList, {
id: "accessList",
header: intl.formatMessage({ id: "column.access" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.enabled, {
id: "enabled",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onEdit, onDisableToggle, onDelete],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useProxyHosts } from "src/hooks";
import { T } from "src/locale";
import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals";
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
import { showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_list", "certificate"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteProxyHost(id);
showObjectSuccess("proxy-host", "deleted");
};
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleProxyHost(id, enabled);
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
showObjectSuccess("proxy-host", enabled ? "enabled" : "disabled");
};
let filtered = null;
if (search && data) {
filtered = data?.filter(
(item) =>
item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
item.forwardHost.toLowerCase().includes(search) ||
`${item.forwardPort}`.includes(search),
);
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("ProxyHosts", "lime")}>
{data?.length ? (
showProxyHostModal("new")}
>
) : null}
showProxyHostModal(id)}
onDelete={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["proxy-hosts"], ["proxy-host", id]],
children: ,
})
}
onDisableToggle={handleDisableToggle}
onNew={() => showProxyHostModal("new")}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/ProxyHosts/index.tsx
================================================
import { HasPermission } from "src/components";
import { PROXY_HOSTS, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const ProxyHosts = () => {
return (
);
};
export default ProxyHosts;
================================================
FILE: frontend/src/pages/Nginx/RedirectionHosts/Table.tsx
================================================
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { RedirectionHost } from "src/api/backend";
import {
CertificateFormatter,
DomainsFormatter,
EmptyData,
GravatarFormatter,
HasPermission,
TrueFalseFormatter,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
interface Props {
data: RedirectionHost[];
isFiltered?: boolean;
isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNew?: () => void;
}
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "domainNames",
header: intl.formatMessage({ id: "column.source" }),
cell: (info: any) => {
const value = info.getValue();
return ;
},
}),
columnHelper.accessor((row: any) => row.forwardHttpCode, {
id: "forwardHttpCode",
header: intl.formatMessage({ id: "column.http-code" }),
cell: (info: any) => {
return info.getValue();
},
}),
columnHelper.accessor((row: any) => row.forwardScheme, {
id: "forwardScheme",
header: intl.formatMessage({ id: "column.scheme" }),
cell: (info: any) => {
return info.getValue().toUpperCase();
},
}),
columnHelper.accessor((row: any) => row.forwardDomainName, {
id: "forwardDomainName",
header: intl.formatMessage({ id: "column.destination" }),
cell: (info: any) => {
return info.getValue();
},
}),
columnHelper.accessor((row: any) => row.certificate, {
id: "certificate",
header: intl.formatMessage({ id: "column.ssl" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.enabled, {
id: "enabled",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onEdit, onDisableToggle, onDelete],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useRedirectionHosts } from "src/hooks";
import { T } from "src/locale";
import { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from "src/modals";
import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
import { showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteRedirectionHost(id);
showObjectSuccess("redirection-host", "deleted");
};
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleRedirectionHost(id, enabled);
queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
showObjectSuccess("redirection-host", enabled ? "enabled" : "disabled");
};
let filtered = null;
if (search && data) {
filtered = data?.filter((item) => {
return (
item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) ||
item.forwardDomainName.toLowerCase().includes(search)
);
});
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("RedirectionHosts", "yellow")}>
{data?.length ? (
showRedirectionHostModal("new")}
>
) : null}
showRedirectionHostModal(id)}
onDelete={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["redirection-hosts"], ["redirection-host", id]],
children: ,
})
}
onDisableToggle={handleDisableToggle}
onNew={() => showRedirectionHostModal("new")}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/RedirectionHosts/index.tsx
================================================
import { HasPermission } from "src/components";
import { REDIRECTION_HOSTS, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const RedirectionHosts = () => {
return (
);
};
export default RedirectionHosts;
================================================
FILE: frontend/src/pages/Nginx/Streams/Table.tsx
================================================
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { Stream } from "src/api/backend";
import {
CertificateFormatter,
EmptyData,
GravatarFormatter,
HasPermission,
TrueFalseFormatter,
ValueWithDateFormatter,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
import { MANAGE, STREAMS } from "src/modules/Permissions";
interface Props {
data: Stream[];
isFiltered?: boolean;
isFetching?: boolean;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNew?: () => void;
}
export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onDisableToggle, onNew }: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "incomingPort",
header: intl.formatMessage({ id: "column.incoming-port" }),
cell: (info: any) => {
const value = info.getValue();
return ;
},
}),
columnHelper.accessor((row: any) => row, {
id: "forwardHttpCode",
header: intl.formatMessage({ id: "column.destination" }),
cell: (info: any) => {
const value = info.getValue();
return `${value.forwardingHost}:${value.forwardingPort}`;
},
}),
columnHelper.accessor((row: any) => row, {
id: "tcpForwarding",
header: intl.formatMessage({ id: "column.protocol" }),
cell: (info: any) => {
const value = info.getValue();
return (
<>
{value.tcpForwarding ? (
) : null}
{value.udpForwarding ? (
) : null}
>
);
},
}),
columnHelper.accessor((row: any) => row.certificate, {
id: "certificate",
header: intl.formatMessage({ id: "column.ssl" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.enabled, {
id: "enabled",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[columnHelper, onEdit, onDisableToggle, onDelete],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/Streams/TableWrapper.tsx
================================================
import { IconHelp, IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteStream, toggleStream } from "src/api/backend";
import { Button, HasPermission, LoadingPage } from "src/components";
import { useStreams } from "src/hooks";
import { T } from "src/locale";
import { showDeleteConfirmModal, showHelpModal, showStreamModal } from "src/modals";
import { MANAGE, STREAMS } from "src/modules/Permissions";
import { showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const [_deleteId, _setDeleteIdd] = useState(0);
const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]);
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleDelete = async (id: number) => {
await deleteStream(id);
showObjectSuccess("stream", "deleted");
};
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleStream(id, enabled);
queryClient.invalidateQueries({ queryKey: ["streams"] });
queryClient.invalidateQueries({ queryKey: ["stream", id] });
showObjectSuccess("stream", enabled ? "enabled" : "disabled");
};
let filtered = null;
if (search && data) {
filtered = data?.filter((item) => {
return (
`${item.incomingPort}`.includes(search) ||
`${item.forwardingPort}`.includes(search) ||
item.forwardingHost.includes(search)
);
});
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
setSearch(e.target.value.toLowerCase().trim())}
/>
) : null}
showHelpModal("Streams", "blue")}>
{data?.length ? (
showStreamModal("new")}>
) : null}
showStreamModal(id)}
onDelete={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["streams"], ["stream", id]],
children: ,
})
}
onDisableToggle={handleDisableToggle}
onNew={() => showStreamModal("new")}
/>
);
}
================================================
FILE: frontend/src/pages/Nginx/Streams/index.tsx
================================================
import { HasPermission } from "src/components";
import { STREAMS, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const Streams = () => {
return (
);
};
export default Streams;
================================================
FILE: frontend/src/pages/Settings/DefaultSite.tsx
================================================
import CodeEditor from "@uiw/react-textarea-code-editor";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import { Button, Loading } from "src/components";
import { useSetSetting, useSetting } from "src/hooks";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
export default function DefaultSite() {
const { data, isLoading, error } = useSetting("default-site");
const { mutate: setSetting } = useSetSetting();
const [errorMsg, setErrorMsg] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
setErrorMsg(null);
const payload = {
id: "default-site",
value: values.value,
meta: {
redirect: values.redirect,
html: values.html,
},
};
setSetting(payload, {
onError: (err: any) => setErrorMsg( ),
onSuccess: () => {
showObjectSuccess("setting", "saved");
},
onSettled: () => {
setIsSubmitting(false);
setSubmitting(false);
},
});
};
if (!isLoading && error) {
return (
);
}
if (isLoading) {
return (
);
}
return (
{({ values }) => (
)}
);
}
================================================
FILE: frontend/src/pages/Settings/Layout.tsx
================================================
import { T } from "src/locale";
import DefaultSite from "./DefaultSite";
export default function Layout() {
// Taken from https://preview.tabler.io/settings.html
// Refer to that when updating this content
return (
);
}
================================================
FILE: frontend/src/pages/Settings/index.tsx
================================================
import { HasPermission } from "src/components";
import { ADMIN, VIEW } from "src/modules/Permissions";
import Layout from "./Layout";
const Settings = () => {
return (
);
};
export default Settings;
================================================
FILE: frontend/src/pages/Setup/index.module.css
================================================
.logo {
width: 200px;
}
.helperBtns {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
================================================
FILE: frontend/src/pages/Setup/index.tsx
================================================
import { useQueryClient } from "@tanstack/react-query";
import cn from "classnames";
import { Field, Form, Formik } from "formik";
import { useState } from "react";
import { Alert } from "react-bootstrap";
import { createUser } from "src/api/backend";
import { Button, LocalePicker, Page, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context";
import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
import styles from "./index.module.css";
interface Payload {
name: string;
email: string;
password: string;
}
export default function Setup() {
const queryClient = useQueryClient();
const { login } = useAuthState();
const [errorMsg, setErrorMsg] = useState(null);
const onSubmit = async (values: Payload, { setSubmitting }: any) => {
setErrorMsg(null);
// Set a nickname, which is the first word of the name
const nickname = values.name.split(" ")[0];
const { password, ...payload } = {
...values,
...{
nickname,
auth: {
type: "password",
secret: values.password,
},
},
};
try {
const user = await createUser(payload, true);
if (user?.id) {
try {
await login(user.email, password);
// Trigger a Health change
await queryClient.refetchQueries({ queryKey: ["health"] });
// window.location.reload();
} catch (err: any) {
setErrorMsg(err.message);
}
} else {
setErrorMsg("cannot_create_user");
}
} catch (err: any) {
setErrorMsg(err.message);
}
setSubmitting(false);
};
return (
setErrorMsg(null)} dismissible>
{errorMsg}
{({ isSubmitting }) => (
)}
);
}
================================================
FILE: frontend/src/pages/Users/Table.tsx
================================================
import {
IconDotsVertical,
IconEdit,
IconLock,
IconLogin2,
IconPower,
IconShield,
IconTrash,
} from "@tabler/icons-react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useMemo } from "react";
import type { User } from "src/api/backend";
import {
EmailFormatter,
EmptyData,
GravatarFormatter,
RolesFormatter,
TrueFalseFormatter,
ValueWithDateFormatter,
} from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
interface Props {
data: User[];
isFiltered?: boolean;
isFetching?: boolean;
currentUserId?: number;
onEditUser?: (id: number) => void;
onEditPermissions?: (id: number) => void;
onSetPassword?: (id: number) => void;
onDeleteUser?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNewUser?: () => void;
onLoginAs?: (id: number) => void;
}
export default function Table({
data,
isFiltered,
isFetching,
currentUserId,
onEditUser,
onEditPermissions,
onSetPassword,
onDeleteUser,
onDisableToggle,
onNewUser,
onLoginAs,
}: Props) {
const columnHelper = createColumnHelper();
const columns = useMemo(
() => [
columnHelper.accessor((row: any) => row, {
id: "avatar",
cell: (info: any) => {
const value = info.getValue();
return ;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: any) => row, {
id: "name",
header: intl.formatMessage({ id: "column.name" }),
cell: (info: any) => {
const value = info.getValue();
// Hack to reuse domains formatter
return (
);
},
}),
columnHelper.accessor((row: any) => row.email, {
id: "email",
header: intl.formatMessage({ id: "column.email" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.roles, {
id: "roles",
header: intl.formatMessage({ id: "column.roles" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.accessor((row: any) => row.isDisabled, {
id: "isDisabled",
header: intl.formatMessage({ id: "column.status" }),
cell: (info: any) => {
return ;
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => {
return (
);
},
meta: {
className: "text-end w-1",
},
}),
],
[
columnHelper,
currentUserId,
onEditUser,
onDisableToggle,
onDeleteUser,
onEditPermissions,
onSetPassword,
onLoginAs,
],
);
const tableInstance = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
rowCount: data.length,
meta: {
isFetching,
},
enableSortingRemoval: false,
});
return (
}
/>
);
}
================================================
FILE: frontend/src/pages/Users/TableWrapper.tsx
================================================
import { IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import Alert from "react-bootstrap/Alert";
import { deleteUser, toggleUser } from "src/api/backend";
import { Button, LoadingPage } from "src/components";
import { useAuthState } from "src/context";
import { useUser, useUsers } from "src/hooks";
import { T } from "src/locale";
import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
import { showError, showObjectSuccess } from "src/notifications";
import Table from "./Table";
export default function TableWrapper() {
const queryClient = useQueryClient();
const { loginAs } = useAuthState();
const [search, setSearch] = useState("");
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
const { data: currentUser } = useUser("me");
if (isLoading) {
return ;
}
if (isError) {
return {error?.message || "Unknown error"} ;
}
const handleLoginAs = async (id: number) => {
try {
await loginAs(id);
} catch (err) {
if (err instanceof Error) {
showError(err.message);
}
}
};
const handleDelete = async (id: number) => {
await deleteUser(id);
showObjectSuccess("user", "deleted");
};
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleUser(id, enabled);
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["user", id] });
showObjectSuccess("user", enabled ? "enabled" : "disabled");
};
let filtered = null;
if (search && data) {
filtered = data?.filter((item) => {
return (
item.name.toLowerCase().includes(search) ||
item.nickname.toLowerCase().includes(search) ||
item.email.toLowerCase().includes(search)
);
});
} else if (search !== "") {
// this can happen if someone deletes the last item while searching
setSearch("");
}
return (
{data?.length ? (
) : null}
showUserModal(id)}
onEditPermissions={(id: number) => showPermissionsModal(id)}
onSetPassword={(id: number) => showSetPasswordModal(id)}
onDeleteUser={(id: number) =>
showDeleteConfirmModal({
title: ,
onConfirm: () => handleDelete(id),
invalidations: [["users"], ["user", id]],
children: ,
})
}
onDisableToggle={handleDisableToggle}
onNewUser={() => showUserModal("new")}
onLoginAs={handleLoginAs}
/>
);
}
================================================
FILE: frontend/src/pages/Users/index.tsx
================================================
import { HasPermission } from "src/components";
import { ADMIN, VIEW } from "src/modules/Permissions";
import TableWrapper from "./TableWrapper";
const Users = () => {
return (
);
};
export default Users;
================================================
FILE: frontend/src/vite-env.d.ts
================================================
///
================================================
FILE: frontend/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"src/*": [
"./src/*"
],
"test/*": [
"./test/*"
]
}
},
"include": [
"src",
"test"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
================================================
FILE: frontend/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: frontend/vite.config.ts
================================================
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import checker from "vite-plugin-checker";
import tsconfigPaths from "vite-tsconfig-paths";
import "vitest/config";
import { execFile } from "node:child_process";
const runLocaleScripts = () => {
execFile("yarn", ["locale-compile"], (error, stdout, _stderr) => {
if (error) {
throw error;
}
console.log(stdout);
execFile("yarn", ["locale-sort"], (error, stdout, _stderr) => {
if (error) {
throw error;
}
console.log(stdout);
});
});
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
{
name: 'run-on-start',
configureServer(_server) {
runLocaleScripts();
},
},
{
name: "trigger-on-reload",
configureServer(server) {
server.watcher.on("change", (file) => {
if (file.includes("locale/src")) {
console.log(`File changed: ${file}, running locale scripts...`);
runLocaleScripts();
}
});
},
},
react(),
checker({
// e.g. use TypeScript check
typescript: true,
}),
tsconfigPaths(),
],
server: {
host: true,
port: 5173,
strictPort: true,
allowedHosts: true,
},
test: {
environment: "happy-dom",
setupFiles: ["./vitest-setup.js"],
},
assetsInclude: ["**/*.md", "**/*.png", "**/*.svg"],
});
================================================
FILE: frontend/vitest-setup.js
================================================
import "@testing-library/jest-dom/vitest";
================================================
FILE: scripts/.common.sh
================================================
#!/bin/bash
# Colors
BLUE='\E[1;34m'
CYAN='\E[1;36m'
GREEN='\E[1;32m'
RED='\E[1;31m'
RESET='\E[0m'
YELLOW='\E[1;33m'
export BLUE CYAN GREEN RED RESET YELLOW
# Docker Compose
COMPOSE_PROJECT_NAME="npm2dev"
COMPOSE_FILE="docker/docker-compose.dev.yml"
export COMPOSE_FILE COMPOSE_PROJECT_NAME
# $1: container_name
get_container_ip () {
local container_name=$1
local container
local ip
container=$(docker compose ps --all -q "${container_name}" | tail -n1)
ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
echo "$ip"
}
================================================
FILE: scripts/buildx
================================================
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
echo -e "${BLUE}❯ ${CYAN}Building docker multiarch: ${YELLOW}${*}${RESET}"
cd "${DIR}/.." || exit 1
# determine commit if not already set
if [ "$BUILD_COMMIT" == "" ]; then
BUILD_COMMIT=$(git log -n 1 --format=%h)
fi
# Buildx Builder
docker buildx create --name "${BUILDX_NAME:-npm}" || echo
docker buildx use "${BUILDX_NAME:-npm}"
docker buildx build \
--build-arg BUILD_VERSION="${BUILD_VERSION:-dev}" \
--build-arg BUILD_COMMIT="${BUILD_COMMIT:-notset}" \
--build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \
--build-arg GOPROXY="${GOPROXY:-}" \
--build-arg GOPRIVATE="${GOPRIVATE:-}" \
--platform linux/amd64,linux/arm64 \
--progress plain \
--pull \
-f docker/Dockerfile \
$@ \
.
rc=$?
docker buildx rm "${BUILDX_NAME:-npm}"
echo -e "${BLUE}❯ ${GREEN}Multiarch build Complete${RESET}"
exit $rc
================================================
FILE: scripts/ci/frontend-build
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/../.common.sh"
DOCKER_IMAGE=nginxproxymanager/nginx-full:certbot-node
# Ensure docker exists
if hash docker 2>/dev/null; then
docker pull "${DOCKER_IMAGE}"
cd "${DIR}/../.."
echo -e "${BLUE}❯ ${CYAN}Building Frontend ...${RESET}"
docker run --rm \
-e CI=true \
-e NODE_OPTIONS=--openssl-legacy-provider \
-v "$(pwd)/frontend:/app/frontend" \
-w /app/frontend "${DOCKER_IMAGE}" \
sh -c "yarn install && yarn lint && yarn locale-compile && yarn vitest run --no-color && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}"
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/ci/fulltest-cypress
================================================
#!/bin/bash
set -e
STACK="${1:-sqlite}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# remember this is running in "ci" folder..
# Some defaults for running this script outside of CI
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-npm_local_fulltest}"
export IMAGE="${IMAGE:-nginx-proxy-manager}"
export BRANCH_LOWER="${BRANCH_LOWER:-unknown}"
export BUILD_NUMBER="${BUILD_NUMBER:-0000}"
if [ "${COMPOSE_FILE:-}" = "" ]; then
export COMPOSE_FILE="docker/docker-compose.ci.yml:docker/docker-compose.ci.${STACK}.yml"
fi
# Colors
BLUE='\E[1;34m'
RED='\E[1;31m'
CYAN='\E[1;36m'
GREEN='\E[1;32m'
RESET='\E[0m'
YELLOW='\E[1;33m'
export BLUE CYAN GREEN RESET YELLOW
echo -e "${BLUE}❯ ${CYAN}Starting fullstack cypress testing ...${RESET}"
echo -e "${BLUE}❯ $(docker compose config)${RESET}"
# $1: container_name
get_container_ip () {
local container_name=$1
local container
local ip
container=$(docker compose ps --all -q "${container_name}" | tail -n1)
ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
echo "$ip"
}
# Bring up a stack, in steps so we can inject IPs everywhere
docker compose up -d pdns pdns-db
PDNS_IP=$(get_container_ip "pdns")
echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}"
# adjust the dnsrouter config
LOCAL_DNSROUTER_CONFIG="$DIR/../../docker/dev/dnsrouter-config.json"
rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp"
# IMPORTANT: changes to dnsrouter-config.json will affect this line:
jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp"
docker compose up -d dnsrouter
DNSROUTER_IP=$(get_container_ip "dnsrouter")
echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}"
if [ "${DNSROUTER_IP:-}" = "" ]; then
echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}"
exit 1
fi
# mount the resolver
LOCAL_RESOLVE="$DIR/../../docker/dev/resolv.conf"
rm -rf "${LOCAL_RESOLVE}"
printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
# bring up all remaining containers, except cypress!
docker compose up -d --remove-orphans stepca squid
docker compose pull db-mysql || true # ok to fail
docker compose pull db-postgres || true # ok to fail
docker compose pull authentik authentik-redis authentik-ldap || true # ok to fail
docker compose up -d --remove-orphans --pull=never fullstack
# wait for main container to be healthy
bash "$DIR/../wait-healthy" "$(docker compose ps --all -q fullstack)" 120
# Run tests
rm -rf "$DIR/../../test/results"
docker compose up --build cypress
# Get results
docker cp -L "$(docker compose ps --all -q cypress):/test/results" "$DIR/../../test/"
docker cp -L "$(docker compose ps --all -q fullstack):/data/logs" "$DIR/../../test/results/"
if [ "$2" = "cleanup" ]; then
echo -e "${BLUE}❯ ${CYAN}Cleaning up containers ...${RESET}"
docker compose down --remove-orphans --volumes -t 30
fi
echo -e "${BLUE}❯ ${GREEN}Fullstack cypress testing complete${RESET}"
================================================
FILE: scripts/ci/test-and-build
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/../.common.sh"
TESTING_IMAGE=nginxproxymanager/nginx-full:certbot-node
docker pull "${TESTING_IMAGE}"
# Test
echo -e "${BLUE}❯ ${CYAN}Testing backend ...${RESET}"
docker run --rm \
-v "$(pwd)/backend:/app" \
-w /app \
"${TESTING_IMAGE}" \
sh -c 'yarn install && yarn lint . && rm -rf node_modules'
echo -e "${BLUE}❯ ${GREEN}Testing Complete${RESET}"
# Build
echo -e "${BLUE}❯ ${CYAN}Building ...${RESET}"
docker build --pull --no-cache --compress \
-t "${IMAGE:-nginx-proxy-manager}:${BRANCH_LOWER:-unknown}-ci-${BUILD_NUMBER:-0000}" \
-f docker/Dockerfile \
--progress=plain \
--build-arg TARGETPLATFORM=linux/amd64 \
--build-arg BUILDPLATFORM=linux/amd64 \
--build-arg BUILD_VERSION="${BUILD_VERSION:-unknown}" \
--build-arg BUILD_COMMIT="${BUILD_COMMIT:-unknown}" \
--build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \
.
echo -e "${BLUE}❯ ${GREEN}Building Complete${RESET}"
================================================
FILE: scripts/cypress-dev
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Ensure docker exists
if hash docker 2>/dev/null; then
cd "${DIR}/.."
rm -rf "$DIR/../test/results"
docker compose up --build cypress
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/destroy-dev
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Make sure docker exists
if hash docker 2>/dev/null; then
cd "${DIR}/.."
echo -e "${BLUE}❯ ${CYAN}Destroying Dev Stack ...${RESET}"
docker compose down --remove-orphans --volumes
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/docs-build
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Ensure docker exists
if hash docker 2>/dev/null; then
cd "${DIR}/.."
echo -e "${BLUE}❯ ${CYAN}Building Docs ...${RESET}"
docker 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"
echo -e "${BLUE}❯ ${GREEN}Building Docs Complete${RESET}"
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/docs-upload
================================================
#!/bin/bash
# Note: This script is designed to be run inside CI builds
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
echo -e "${BLUE}❯ ${CYAN}Uploading docs in: ${YELLOW}$1${RESET}"
cd "$1" || exit 1
ALL_FILES=$(find . -follow)
for FILE in $ALL_FILES
do
# remove preceding ./
FILE=$(echo "$FILE" | sed -E "s/\.\///g")
echo '======================================='
echo "FILE: $FILE"
if [[ -d $FILE ]]; then
echo "Skipping $FILE because it's a directory"
elif [[ -f $FILE ]]; then
PARAM_STRING="--guess-mime-type"
EXT="${FILE##*.}"
if [ "$EXT" == "css" ]; then
PARAM_STRING="-mtext/css"
elif [ "$EXT" == "js" ]; then
PARAM_STRING="-mapplication/javascript"
elif [[ "$EXT" == "html" ]]; then
PARAM_STRING="-mtext/html"
elif [[ "$EXT" == "png" ]]; then
PARAM_STRING="-mimage/png"
elif [[ "$EXT" == "jpg" ]]; then
PARAM_STRING="-mimage/jpg"
elif [[ "$EXT" == "svg" ]]; then
PARAM_STRING="-mimage/svg+xml"
fi
DEST_FOLDER=$(dirname "$FILE")
if [ "$DEST_FOLDER" == "." ]; then
DEST_FOLDER=
else
DEST_FOLDER="${DEST_FOLDER}/"
fi
echo s3cmd -v -f -P "$PARAM_STRING" --add-header="Cache-Control:public,max-age=604800" sync "$FILE" "s3://$S3_BUCKET/$DEST_FOLDER"
s3cmd -v -f -P "$PARAM_STRING" --add-header="Cache-Control:public,max-age=604800" sync "$FILE" "s3://$S3_BUCKET/$DEST_FOLDER"
rc=$?; if [ $rc != 0 ]; then exit $rc; fi
fi
done
echo -e "${BLUE}❯ ${GREEN}Upload Complete${RESET}"
================================================
FILE: scripts/start-dev
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Ensure docker exists
if hash docker 2>/dev/null; then
cd "${DIR}/.."
echo -e "${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}"
echo -e "${BLUE}❯ $(docker compose config)${RESET}"
# Bring up a stack, in steps so we can inject IPs everywhere
docker compose up -d pdns pdns-db
PDNS_IP=$(get_container_ip "pdns")
echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}"
# adjust the dnsrouter config
LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json"
rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp"
# IMPORTANT: changes to dnsrouter-config.json will affect this line:
jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp"
docker compose up -d dnsrouter
DNSROUTER_IP=$(get_container_ip "dnsrouter")
echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}"
if [ "${DNSROUTER_IP:-}" = "" ]; then
echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}"
exit 1
fi
# mount the resolver
LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf"
rm -rf "${LOCAL_RESOLVE}"
printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
# bring up all remaining containers, except cypress!
docker compose up -d --remove-orphans stepca squid
docker compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap
docker compose build --pull --parallel fullstack
docker compose up -d --remove-orphans fullstack
docker compose up -d --remove-orphans swagger
# wait for main container to be healthy
bash "$DIR/wait-healthy" "$(docker compose ps --all -q fullstack)" 120
echo ""
echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}"
echo -e "${CYAN}Nginx: http://127.0.0.1:3080${RESET}"
echo -e "${CYAN}Swagger Doc: http://127.0.0.1:3001${RESET}"
echo ""
if [ "$1" == "-f" ]; then
echo -e "${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}"
docker logs -f npm2dev.core
else
echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:"
echo " docker logs -f npm2dev.core"
fi
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/stop-dev
================================================
#!/bin/bash -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
# Make sure docker exists
if hash docker 2>/dev/null; then
cd "${DIR}/.."
echo -e "${BLUE}❯ ${CYAN}Stopping Dev Stack ...${RESET}"
docker compose down --remove-orphans
else
echo -e "${RED}❯ docker command is not available${RESET}"
fi
================================================
FILE: scripts/wait-healthy
================================================
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$DIR/.common.sh"
if [ "$1" == "" ]; then
echo "Waits for a docker container to be healthy."
echo "Usage: $0 docker-container"
exit 1
fi
SERVICE=$1
LOOPCOUNT=0
HEALTHY=
LIMIT=${2:-90}
echo -e "${BLUE}❯ ${CYAN}Waiting for healthy: ${YELLOW}${SERVICE}${RESET}"
until [ "${HEALTHY}" = "healthy" ]; do
echo -n "."
sleep 1
HEALTHY="$(docker inspect -f '{{.State.Health.Status}}' $SERVICE)"
((LOOPCOUNT++))
if [ "$LOOPCOUNT" == "$LIMIT" ]; then
echo -e "${BLUE}❯ ${RED}Timed out waiting for healthy${RESET}"
docker logs --tail 50 "$SERVICE"
exit 1
fi
done
echo ""
echo -e "${BLUE}❯ ${GREEN}Healthy!${RESET}"
================================================
FILE: test/.eslintrc.json
================================================
{
"env": {
"browser": true,
"es6": true,
"cypress/globals": true
},
"extends": [
"eslint:recommended",
"plugin:cypress/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"cypress",
"chai-friendly",
"align-assignments"
],
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"key-spacing": [
"error",
{
"align": "value"
}
],
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"func-call-spacing": [
"error",
"never"
],
"keyword-spacing": [
"error",
{
"before": true
}
],
"no-irregular-whitespace": "error",
"cypress/no-assigning-return-values": "error",
"cypress/no-unnecessary-waiting": "warn",
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": 2,
"align-assignments/align-assignments": [
2,
{
"requiresOnly": false
}
]
}
}
================================================
FILE: test/.gitignore
================================================
results/*
cypress/results/*
================================================
FILE: test/.prettierrc
================================================
{
"printWidth": 160,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "all",
"proseWrap": "always"
}
================================================
FILE: test/README.md
================================================
# Cypress Test Suite
## Running Locally
```
cd nginxproxymanager/test
yarn install
yarn run cypress
```
## VS Code
Editor 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,
so 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
"workspace" as each project is different.
### ESLint
The ESLint extension only works on JavaScript files by default, so add the following to your workspace settings and reload VSCode.
```
"eslint.autoFixOnSave": true,
"eslint.validate": [
{ "language": "javascript", "autoFix": true },
"html"
]
```
> 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
> commands on save for JavaScript and TypeScript files:
```
"editor.formatOnSave": true,
"[javascript]": {
"editor.formatOnSave": false,
},
"[javascriptreact]": {
"editor.formatOnSave": false,
},
"[typescript]": {
"editor.formatOnSave": false,
},
"[typescriptreact]": {
"editor.formatOnSave": false,
},
```
================================================
FILE: test/cypress/Dockerfile
================================================
FROM cypress/included:15.11.0
# Disable Cypress CLI colors
ENV FORCE_COLOR=0
ENV NO_COLOR=1
# testssl.sh and mkcert
RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \
&& tar -xzf /tmp/testssl.tgz -C /tmp \
&& mv /tmp/testssl.sh-3.2rc4 /testssl \
&& rm /tmp/testssl.tgz \
&& apt-get update \
&& apt-get install -y bsdmainutils curl dnsutils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \
&& chmod +x /bin/mkcert
COPY --chown=1000 ./test /test
WORKDIR /test
RUN yarn install && yarn cache clean
ENTRYPOINT []
CMD ["cypress", "run"]
================================================
FILE: test/cypress/config/ci.mjs
================================================
import { defineConfig } from 'cypress';
import pluginSetup from '../plugins/index.mjs';
export default defineConfig({
requestTimeout: 30000,
defaultCommandTimeout: 20000,
reporter: "cypress-multi-reporters",
reporterOptions: {
configFile: "multi-reporter.json"
},
video: true,
videosFolder: "results/videos",
screenshotsFolder: "results/screenshots",
e2e: {
setupNodeEvents(on, config) {
return pluginSetup(on, config);
},
env: {
swaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`,
},
baseUrl: "http://fullstack:81",
}
});
================================================
FILE: test/cypress/config/dev.mjs
================================================
import { defineConfig } from 'cypress';
import pluginSetup from '../plugins/index.mjs';
export default defineConfig({
requestTimeout: 30000,
defaultCommandTimeout: 20000,
reporter: "cypress-multi-reporters",
reporterOptions: {
configFile: "multi-reporter.json"
},
video: true,
videosFolder: "results/videos",
screenshotsFolder: "results/screenshots",
e2e: {
setupNodeEvents(on, config) {
return pluginSetup(on, config);
},
env: {
swaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`,
},
baseUrl: "http://127.0.0.1:3081",
}
});
================================================
FILE: test/cypress/e2e/api/Certificates.cy.js
================================================
///
describe('Certificates endpoints', () => {
let token;
let certID;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Validate custom certificate', () => {
cy.task('backendApiPostFiles', {
token: token,
path: '/api/nginx/certificates/validate',
files: {
certificate: 'test.example.com.pem',
certificate_key: 'test.example.com-key.pem',
},
}).then((data) => {
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data);
expect(data).to.have.property('certificate');
expect(data).to.have.property('certificate_key');
});
});
it('Custom certificate lifecycle', () => {
// Create custom cert
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
provider: "other",
nice_name: "Test Certificate",
},
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
certID = data.id;
// Upload files
cy.task('backendApiPostFiles', {
token: token,
path: `/api/nginx/certificates/${certID}/upload`,
files: {
certificate: 'test.example.com.pem',
certificate_key: 'test.example.com-key.pem',
},
}).then((data) => {
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
expect(data).to.have.property('certificate');
expect(data).to.have.property('certificate_key');
// Get all certs
cy.task('backendApiGet', {
token: token,
path: '/api/nginx/certificates?expand=owner'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);
expect(data.length).to.be.greaterThan(0);
// Delete cert
cy.task('backendApiDelete', {
token: token,
path: `/api/nginx/certificates/${certID}`
}).then((data) => {
cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);
expect(data).to.be.equal(true);
});
});
});
});
});
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
meta: {
dns_challenge: false,
},
provider: 'letsencrypt',
},
returnOnError: true,
}).then((data) => {
cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);
expect(data).to.have.property('error');
expect(data.error).to.have.property('message');
expect(data.error).to.have.property('code');
expect(data.error.code).to.equal(400);
expect(data.error.message).to.contain('data/domain_names/0 must match pattern');
});
});
});
================================================
FILE: test/cypress/e2e/api/Dashboard.cy.js
================================================
///
describe('Dashboard endpoints', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to get host counts', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/reports/hosts'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/reports/hosts', data);
expect(data).to.have.property('dead');
expect(data).to.have.property('proxy');
expect(data).to.have.property('redirection');
expect(data).to.have.property('stream');
});
});
});
================================================
FILE: test/cypress/e2e/api/FullCertProvision.cy.js
================================================
///
describe('Full Certificate Provisions', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to create new http certificate', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: [
'website1.example.com'
],
meta: {
dns_challenge: false
},
provider: 'letsencrypt'
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.provider).to.be.equal('letsencrypt');
});
});
it('Should be able to create new DNS certificate with Powerdns', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: [
'website2.example.com'
],
meta: {
dns_challenge: true,
dns_provider: 'powerdns',
dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm',
propagation_seconds: 5,
},
provider: 'letsencrypt'
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.provider).to.be.equal('letsencrypt');
expect(data.meta.dns_provider).to.be.equal('powerdns');
});
});
});
================================================
FILE: test/cypress/e2e/api/Health.cy.js
================================================
///
describe('Basic API checks', () => {
it('Should return a valid health payload', () => {
cy.task('backendApiGet', {
path: '/api/',
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('get', 200, '/', data);
});
});
it('Should return a valid schema payload', () => {
cy.task('backendApiGet', {
path: `/api/schema?ts=${Date.now()}`,
}).then((data) => {
expect(data.openapi).to.be.equal('3.1.0');
});
});
it('Should return a valid payload for version check', () => {
cy.task('backendApiGet', {
path: `/api/version/check?ts=${Date.now()}`,
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/version/check', data);
});
});
});
================================================
FILE: test/cypress/e2e/api/Ldap.cy.js
================================================
///
describe('LDAP with Authentik', () => {
let _token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
_token = tok;
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/ldap-auth',
// data: {
// value: {
// host: 'authentik-ldap:3389',
// base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io',
// user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io',
// email_property: 'mail',
// name_property: 'sn',
// self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))',
// auto_create_user: true
// }
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/auth-methods',
// data: {
// value: [
// 'local',
// 'ldap'
// ]
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
});
});
it.skip('Should log in with LDAP', () => {
// cy.task('backendApiPost', {
// token: token,
// path: '/api/auth',
// data: {
// // Authentik LDAP creds:
// type: 'ldap',
// identity: 'cypress',
// secret: 'fqXBfUYqHvYqiwBHWW7f'
// }
// }).then((data) => {
// cy.validateSwaggerSchema('post', 200, '/auth', data);
// expect(data.result).to.have.property('token');
// });
});
}
});
================================================
FILE: test/cypress/e2e/api/OAuth.cy.js
================================================
///
describe('OAuth with Authentik', () => {
let _token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => {
cy.getToken().then((tok) => {
_token = tok;
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/oauth-auth',
// data: {
// value: {
// client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU',
// client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq',
// authorization_url: 'http://authentik:9000/application/o/authorize/',
// resource_url: 'http://authentik:9000/application/o/userinfo/',
// token_url: 'http://authentik:9000/application/o/token/',
// logout_url: 'http://authentik:9000/application/o/npm/end-session/',
// identifier: 'preferred_username',
// scopes: [],
// auto_create_user: true
// }
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/auth-methods',
// data: {
// value: [
// 'local',
// 'oauth'
// ]
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
});
});
it.skip('Should log in with OAuth', () => {
// cy.task('backendApiGet', {
// path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')),
// }).then((data) => {
// expect(data).to.have.property('result');
// cy.origin('http://authentik:9000', {args: data.result}, (url) => {
// cy.visit(url);
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-identification')
// .shadow()
// .find('input[name="uidField"]', { visible: true })
// .type('cypress');
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-identification')
// .shadow()
// .find('button[type="submit"]', { visible: true })
// .click();
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-password')
// .shadow()
// .find('input[name="password"]', { visible: true })
// .type('fqXBfUYqHvYqiwBHWW7f');
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-password')
// .shadow()
// .find('button[type="submit"]', { visible: true })
// .click();
// })
// // we should be logged in
// cy.get('#root p.chakra-text')
// .first()
// .should('have.text', 'Nginx Proxy Manager');
// // logout:
// cy.clearLocalStorage();
// });
});
}
});
================================================
FILE: test/cypress/e2e/api/ProxyHosts.cy.js
================================================
///
describe('Proxy Hosts endpoints', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to create a http host', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/proxy-hosts',
data: {
domain_names: ['test.example.com'],
forward_scheme: 'http',
forward_host: '1.1.1.1',
forward_port: 80,
access_list_id: '0',
certificate_id: 0,
meta: {
dns_challenge: false
},
advanced_config: '',
locations: [],
block_exploits: false,
caching_enabled: false,
allow_websocket_upgrade: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
ssl_forced: false
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled');
expect(data).to.have.property("enabled", true);
expect(data).to.have.property('meta');
expect(typeof data.meta.nginx_online).to.be.equal('undefined');
});
});
});
================================================
FILE: test/cypress/e2e/api/Settings.cy.js
================================================
///
describe('Settings endpoints', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Get all settings', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/settings',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data.length).to.be.greaterThan(0);
});
});
it('Get default-site setting', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/settings/default-site',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
});
});
it('Default Site congratulations', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'congratulations',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('congratulations');
});
});
it('Default Site 404', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: '404',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('404');
});
});
it('Default Site 444', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: '444',
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('444');
});
});
it('Default Site redirect', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'redirect',
meta: {
redirect: 'https://www.google.com',
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('redirect');
expect(data).to.have.property('meta');
expect(data.meta).to.have.property('redirect');
expect(data.meta.redirect).to.be.equal('https://www.google.com');
});
});
it('Default Site html', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'html',
meta: {
html: 'hello world
'
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.equal('default-site');
expect(data).to.have.property('value');
expect(data.value).to.be.equal('html');
expect(data).to.have.property('meta');
expect(data.meta).to.have.property('html');
expect(data.meta.html).to.be.equal('hello world
');
});
});
});
================================================
FILE: test/cypress/e2e/api/Streams.cy.js
================================================
///
describe('Streams', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
// Set default site content
cy.task('backendApiPut', {
token: token,
path: '/api/settings/default-site',
data: {
value: 'html',
meta: {
html: 'yay it works
'
},
},
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
});
});
// Create a custom cert pair
cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => {
expect(result.exitCode).to.eq(0);
// Install CA
cy.exec('mkcert -install').then((result) => {
expect(result.exitCode).to.eq(0);
});
});
cy.exec('rm -f /test/results/testssl.json');
});
it('Should be able to create TCP Stream', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1500,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {},
tcp_forwarding: true,
udp_forwarding: false
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', false);
cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => {
expect(result.exitCode).to.eq(0);
expect(result.stdout).to.contain('yay it works');
});
});
});
it('Should be able to create UDP Stream', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1501,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {},
tcp_forwarding: false,
udp_forwarding: true
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', false);
expect(data).to.have.property('udp_forwarding', true);
});
});
it('Should be able to create TCP/UDP Stream', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1502,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: 0,
meta: {},
tcp_forwarding: true,
udp_forwarding: true
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('enabled', true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', true);
cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => {
expect(result.exitCode).to.eq(0);
expect(result.stdout).to.contain('yay it works');
});
});
});
it('Should be able to create SSL TCP Stream', () => {
let certID = 0;
// Create custom cert
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
provider: "other",
nice_name: "Custom Certificate for SSL Stream",
},
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
expect(data).to.have.property('id');
certID = data.id;
// Upload files
cy.task('backendApiPostFiles', {
token: token,
path: `/api/nginx/certificates/${certID}/upload`,
files: {
certificate: 'website1.pem',
certificate_key: 'website1.key.pem',
},
}).then((data) => {
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
expect(data).to.have.property('certificate');
expect(data).to.have.property('certificate_key');
// Create the stream
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/streams',
data: {
incoming_port: 1503,
forwarding_host: '127.0.0.1',
forwarding_port: 80,
certificate_id: certID,
meta: {},
tcp_forwarding: true,
udp_forwarding: false
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/streams', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property("enabled", true);
expect(data).to.have.property('tcp_forwarding', true);
expect(data).to.have.property('udp_forwarding', false);
expect(data).to.have.property('certificate_id', certID);
// Check the ssl termination
cy.task('log', '[testssl.sh] Running ...');
cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', {
timeout: 120000, // 2 minutes
}).then((result) => {
cy.task('log', `[testssl.sh] ${result.stdout}`);
const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"];
const ignoredIDs = [
'cert_chain_of_trust',
'cert_extlifeSpan',
'cert_revocation',
'engine_problem',
'overall_grade',
];
cy.readFile('/test/results/testssl.json').then((data) => {
// Parse each array item
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (ignoredIDs.includes(item.id)) {
continue;
}
expect(item.severity).to.be.oneOf(allowedSeverities);
}
});
});
});
});
});
});
it('Should be able to List Streams', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/nginx/streams?expand=owner,certificate',
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/nginx/streams', data);
expect(data.length).to.be.greaterThan(0);
expect(data[0]).to.have.property('id');
expect(data[0]).to.have.property('enabled');
});
});
});
================================================
FILE: test/cypress/e2e/api/SwaggerSchema.cy.js
================================================
///
describe('Swagger Schema Linting', () => {
it('Should be a completely valid schema', () => {
cy.validateSwaggerFile('/api/schema', 'results/swagger-schema.json');
});
});
================================================
FILE: test/cypress/e2e/api/Users.cy.js
================================================
///
describe('Users endpoints', () => {
let token;
before(() => {
cy.resetUsers();
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to get yourself', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/users/me'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
});
});
it('Should be able to get all users', () => {
cy.task('backendApiGet', {
token: token,
path: '/api/users'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data.length).to.be.greaterThan(0);
});
});
it('Should be able to update yourself', () => {
cy.task('backendApiPut', {
token: token,
path: '/api/users/me',
data: {
name: 'changed name'
}
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.name).to.be.equal('changed name');
});
});
});
================================================
FILE: test/cypress/fixtures/test.example.com-key.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd
qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w
rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge
Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ
oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z
Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X
zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU
ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6
YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe
a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu
W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw
o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW
H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+
N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh
ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU
MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31
qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq
cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9
vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO
utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V
g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1
mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq
YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8
EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk
8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM
RrKmPK/msHKK/sVHiL+NFqo=
-----END PRIVATE KEY-----
================================================
FILE: test/cypress/fixtures/test.example.com.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw
gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1
cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD
DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu
b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe
bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93
QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu
GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+
2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU
QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB
Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE
OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G
A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB
/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t
MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy
l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s
VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn
ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt
IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u
m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV
XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp
1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw
hp8bJUp/QN7pnOVCDbjTQ+HVMXw=
-----END CERTIFICATE-----
================================================
FILE: test/cypress/plugins/backendApi/client.mjs
================================================
import axios from "axios";
import logger from "./logger.mjs";
const BackendApi = function (config, token) {
this.config = config;
this.token = token;
this.axios = axios.create({
baseURL: config.baseUrl,
timeout: 90000,
});
};
/**
* @param {string} token
*/
BackendApi.prototype.setToken = function (token) {
this.token = token;
};
/**
* @param {bool} returnOnError
*/
BackendApi.prototype._prepareOptions = function (returnOnError) {
const options = {
headers: {
Accept: "application/json",
},
};
if (this.token) {
options.headers.Authorization = `Bearer ${this.token}`;
}
if (returnOnError) {
options.validateStatus = () => true;
}
return options;
};
/**
* @param {*} response
* @param {function} resolve
* @param {function} reject
* @param {bool} returnOnError
*/
BackendApi.prototype._handleResponse = (
response,
resolve,
reject,
returnOnError,
) => {
logger("Response data:", response.data);
if (
!returnOnError &&
typeof response.data === "object" &&
typeof response.data.error === "object"
) {
if (
typeof response.data === "object" &&
typeof response.data.error === "object" &&
typeof response.data.error.message !== "undefined"
) {
reject(
new Error(
`${response.data.error.code}: ${response.data.error.message}`,
),
);
} else {
reject(new Error(`Error ${response.status}`));
}
} else {
resolve(response.data);
}
};
/**
* @param {*} err
* @param {function} resolve
* @param {function} reject
* @param {bool} returnOnError
*/
BackendApi.prototype._handleError = (err, resolve, reject, returnOnError) => {
logger("Axios Error:", err);
if (returnOnError) {
resolve(typeof err.response.data !== "undefined" ? err.response.data : err);
} else {
reject(err);
}
};
/**
* @param {string} method
* @param {string} path
* @param {bool} [returnOnError]
* @param {*} [data]
* @returns {Promise}
*/
BackendApi.prototype.request = function (method, path, returnOnError, data) {
logger(method.toUpperCase(), path);
const options = this._prepareOptions(returnOnError);
return new Promise((resolve, reject) => {
const opts = {
method: method,
url: path,
...options,
};
if (data !== undefined && data !== null) {
opts.data = data;
}
this.axios(opts)
.then((response) => {
this._handleResponse(response, resolve, reject, returnOnError);
})
.catch((err) => {
this._handleError(err, resolve, reject, returnOnError);
});
});
};
/**
* @param {string} path
* @param {form} form
* @param {bool} [returnOnError]
* @returns {Promise}
*/
BackendApi.prototype.postForm = function (path, form, returnOnError) {
logger("POST", this.config.baseUrl + path);
const options = this._prepareOptions(returnOnError);
return new Promise((resolve, reject) => {
const opts = {
...options,
...form.getHeaders(),
};
this.axios
.post(path, form, opts)
.then((response) => {
this._handleResponse(response, resolve, reject, returnOnError);
})
.catch((err) => {
this._handleError(err, resolve, reject, returnOnError);
});
});
};
export default BackendApi;
================================================
FILE: test/cypress/plugins/backendApi/logger.mjs
================================================
const log = (...args) => {
const arr = args;
arr.unshift("[Backend API]");
console.log(...arr);
};
export default log;
================================================
FILE: test/cypress/plugins/backendApi/task.mjs
================================================
import fs from "node:fs";
import FormData from "form-data";
import Client from "./client.mjs";
import logger from "./logger.mjs";
export default (config) => {
logger("Client Ready using", config.baseUrl);
return {
/**
* @param {object} options
* @param {string} options.path API path
* @param {string} [options.token] JWT
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiGet: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.request("get", options.path, options.returnOnError || false);
},
/**
* @param {object} options
* @param {string} options.token JWT
* @param {string} options.path API path
* @param {object} options.data
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiPost: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.request(
"post",
options.path,
options.returnOnError || false,
options.data,
);
},
/**
* @param {object} options
* @param {string} options.token JWT
* @param {string} options.path API path
* @param {object} options.files
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiPostFiles: (options) => {
const api = new Client(config);
api.setToken(options.token);
const form = new FormData();
for (const [key, value] of Object.entries(options.files)) {
form.append(
key,
fs.createReadStream(`${config.fixturesFolder}/${value}`),
);
}
return api.postForm(options.path, form, options.returnOnError || false);
},
/**
* @param {object} options
* @param {string} options.token JWT
* @param {string} options.path API path
* @param {object} options.data
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiPut: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.request(
"put",
options.path,
options.returnOnError || false,
options.data,
);
},
/**
* @param {object} options
* @param {string} options.token JWT
* @param {string} options.path API path
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiDelete: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.request(
"delete",
options.path,
options.returnOnError || false,
);
},
};
};
================================================
FILE: test/cypress/plugins/index.mjs
================================================
import { SwaggerValidation } from "@jc21/cypress-swagger-validation";
import chalk from "chalk";
import backendTask from "./backendApi/task.mjs";
export default (on, config) => {
// Replace swaggerBase config var wildcard
if (typeof config.env.swaggerBase !== "undefined") {
config.env.swaggerBase = config.env.swaggerBase.replace(
"{{baseUrl}}",
config.baseUrl,
);
}
// Plugin Events
on("task", SwaggerValidation(config));
on("task", backendTask(config));
on("task", {
log(message) {
console.log(
`${chalk.cyan.bold("[")}${chalk.blue.bold("LOG")}${chalk.cyan.bold("]")} ${chalk.red.bold(message)}`,
);
return null;
},
});
return config;
};
================================================
FILE: test/cypress/support/commands.mjs
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
import 'cypress-wait-until';
Cypress.Commands.add('randomString', (length) => {
let result = '';
const characters = 'ABCDEFGHIJK LMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
});
/**
* Check the swagger schema file:
*
* @param {string} url
* @param {string} savePath
*/
Cypress.Commands.add("validateSwaggerFile", (url, savePath) => {
cy.task('log', `validateSwaggerFile: ${url} -- ${savePath}`)
.then(() => {
return cy
.request(url)
.then((response) => cy.writeFile(savePath, response.body, { log: false }))
.then(() => cy.exec(`yarn swagger-lint '${savePath}'`, { failOnNonZeroExit: false }))
.then((result) => cy.task('log', `Swagger Vacuum Results:\n${result.stdout || ''}`)
.then(() => expect(result.exitCode).to.eq(0)));
});
});
/**
* Check the swagger schema for a specific endpoint:
*
* @param {string} method API Method in swagger doc, "get", "put", "post", "delete"
* @param {integer} code Swagger doc endpoint response code, exactly as defined in swagger doc
* @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc
* @param {*} data The API response data to check against the swagger schema
*/
Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => {
cy.task('validateSwaggerSchema', {
file: Cypress.env('swaggerBase'),
endpoint: path,
method: method,
statusCode: code,
responseSchema: data,
verbose: true
}).should('equal', null);
});
Cypress.Commands.add('createInitialUser', (defaultUser) => {
let user = {
name: 'Cypress McGee',
nickname: 'Cypress',
email: 'cypress@example.com',
auth: {
type: 'password',
secret: 'changeme'
},
};
if (typeof defaultUser === 'object' && defaultUser) {
user = Object.assign({}, user, defaultUser);
}
return cy.task('backendApiPost', {
path: '/api/users',
data: user,
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('post', 201, '/users', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
cy.wrap(data);
});
});
Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => {
if (typeof defaultAuth === 'object' && defaultAuth) {
if (!defaultUser) {
defaultUser = {};
}
defaultUser.auth = defaultAuth;
}
cy.task('backendApiGet', {
path: '/api/',
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('get', 200, '/', data);
if (!data.setup) {
cy.log('Setup = false');
// create a new user
cy.createInitialUser(defaultUser).then(() => {
return cy.getToken(defaultUser);
});
} else {
let auth = {
identity: 'cypress@example.com',
secret: 'changeme',
};
if (typeof defaultUser === 'object' && defaultUser && typeof defaultUser.auth === 'object' && defaultUser.auth) {
auth = Object.assign({}, auth, defaultUser.auth);
}
cy.log('Setup = true');
// login with existing user
cy.task('backendApiPost', {
path: '/api/tokens',
data: auth,
}).then((res) => {
cy.wrap(res.token);
});
}
});
});
Cypress.Commands.add('resetUsers', () => {
cy.task('backendApiDelete', {
path: '/api/users'
}).then((data) => {
expect(data).to.be.equal(true);
cy.wrap(data);
});
});
// TODO: copied from v3, is this usable?
Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeout = 60) => {
cy.log(`Waiting for certificate (${certID}) status (${expected}) timeout (${timeout})`);
cy.waitUntil(() => cy.task('backendApiGet', {
token: token,
path: `/api/certificates/${certID}`
}).then((data) => {
return data.result.status === expected;
}), {
errorMsg: 'Waiting for certificate status failed',
timeout: timeout * 1000,
interval: 5000
});
});
================================================
FILE: test/cypress/support/e2e.js
================================================
import './commands.mjs';
Cypress.on('uncaught:exception', (/*err, runnable*/) => {
// returning false here prevents Cypress from
// failing the test
return false;
});
================================================
FILE: test/jsconfig.json
================================================
{
"include": [
"./node_modules/cypress",
"cypress/**/*.js",
"cypress/config/dev.mjs",
"cypress/config/ci.mjs",
"cypress/plugins/index.mjs",
"cypress/plugins/backendApi/task.mjs",
"cypress/plugins/backendApi/logger.mjs",
"cypress/plugins/backendApi/client.mjs",
"cypress/support/commands.mjs"
]
}
================================================
FILE: test/multi-reporter.json
================================================
{
"reporterEnabled": "spec, mocha-junit-reporter",
"mochaJunitReporterReporterOptions": {
"jenkinsMode": true,
"rootSuiteTitle": "Cypress.npm",
"jenkinsClassnamePrefix": "Cypress.npm.",
"mochaFile": "results/junit/cypress.npm.[hash].xml"
}
}
================================================
FILE: test/package.json
================================================
{
"name": "npm-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@jc21/cypress-swagger-validation": "^0.3.2",
"@quobix/vacuum": "^0.24.0",
"axios": "^1.13.6",
"chalk": "^5.6.2",
"cypress": "^15.11.0",
"cypress-multi-reporters": "^2.0.5",
"cypress-wait-until": "^3.0.2",
"eslint": "^10.0.2",
"eslint-plugin-align-assignments": "^1.1.2",
"eslint-plugin-chai-friendly": "^1.1.0",
"eslint-plugin-cypress": "^6.1.0",
"form-data": "^4.0.5",
"lodash": "^4.17.23",
"mocha": "^11.7.5",
"mocha-junit-reporter": "^2.2.1"
},
"scripts": {
"cypress": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/ci.mjs",
"cypress:headless": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/ci.mjs",
"cypress:dev": "cypress run --config-file=cypress/config/dev.mjs",
"swagger-lint": "vacuum lint -b -q -d -a --no-clip -n=warn"
},
"author": "",
"license": "ISC"
}
================================================
FILE: test/vacuum-rules.yaml
================================================
description: Recommended rules for a high quality specification.
documentationUrl: https://quobix.com/vacuum/rulesets/recommended
rules:
component-description:
category:
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.
id: descriptions
name: Descriptions
description: Component description check
formats:
- oas3
- oas3_1
- oas3_2
given: $
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!
id: component-description
recommended: true
resolved: true
severity: warn
then:
function: oasComponentDescriptions
type: validation
duplicate-paths:
category:
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.
id: operations
name: Operations
description: Paths cannot be duplicated; only the last definition will be kept.
formats:
- oas3
- oas3_1
- oas3_2
given: $
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.
id: duplicate-paths
recommended: true
severity: error
then:
function: duplicatePaths
type: validation
duplicated-entry-in-enum:
category:
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.
id: schemas
name: Schemas
description: Enum values must not have duplicate entry
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
howToFix: Enums need to be unique, you can't duplicate them in the same definition. Please remove the duplicate value.
id: duplicated-entry-in-enum
recommended: true
severity: error
then:
function: duplicatedEnum
type: validation
info-description:
category:
description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.
id: information
name: Contract Information
description: Info section is missing a description
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
howToFix: The 'info' section is missing a description, surely you want people to know what this spec is all about, right?
id: info-description
recommended: true
resolved: true
severity: error
then:
function: infoDescription
type: validation
info-license-spdx:
category:
description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed.
id: information
name: Contract Information
description: License section cannot contain both an identifier and a URL, they are mutually exclusive.
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
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
id: info-license-spdx
recommended: true
resolved: true
severity: error
then:
function: infoLicenseURLSPDX
type: validation
migrate-zally-ignore:
category:
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.
id: validation
name: Validation
description: x-zally-ignore keys should be migrated to x-lint-ignore for compatibility with vacuum
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
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.
id: migrate-zally-ignore
recommended: true
resolved: true
severity: warn
then:
function: migrateZallyIgnore
type: validation
no-$ref-siblings:
category:
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.
id: schemas
name: Schemas
description: $ref values cannot be placed next to other properties (like a description)
formats:
- oas2
- oas3
given: $
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!
id: no-$ref-siblings
recommended: true
severity: error
then:
function: refSiblings
type: validation
no-ambiguous-paths:
category:
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.
id: operations
name: Operations
description: Paths need to resolve unambiguously from one another
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
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.
id: no-ambiguous-paths
recommended: true
resolved: true
severity: error
then:
function: noAmbiguousPaths
type: validation
no-eval-in-markdown:
category:
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.
id: validation
name: Validation
description: Markdown descriptions must not have `eval()` statements'
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
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.
id: no-eval-in-markdown
recommended: true
resolved: true
severity: error
then:
function: noEvalDescription
functionOptions:
pattern: eval\(
type: validation
no-http-verbs-in-path:
category:
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.
id: operations
name: Operations
description: Path segments must not contain an HTTP verb
formats:
- oas3
- oas3_1
- oas3_2
- oas2
given: $
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.
id: no-http-verbs-in-path
recommended: true
severity: warn
then:
function: noVerbsInPath
type: style
no-request-body:
category:
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.
id: operations
name: Operations
description: HTTP GET and DELETE should not accept request bodies
formats:
- oas3
- oas3_1
- oas3_2
given: $
howToFix: Remove 'requestBody' from HTTP GET and DELETE methods
id: no-request-body
recommended: true
severity: warn
then:
function: noRequestBody
type: style
no-script-tags-in-markdown:
category:
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.
id: validation
name: Validation
description: Markdown descriptions must not have `