Showing preview only (1,685K chars total). Download the full file or copy to clipboard to get everything.
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: ''
---
<!--
Are you in the right place?
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*
-->
**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**
<!-- A clear and concise description of what the bug is. -->
**Nginx Proxy Manager Version**
<!-- What version of Nginx Proxy Manager is reported on the login page? -->
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Operating System**
<!-- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. -->
**Additional context**
<!-- Add any other context about the problem here, docker version, browser version, logs if applicable to the problem. Too much info is better than too little. -->
================================================
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?**
<!-- What is this provider called? -->
**Have you checked if a certbot plugin exists?**
<!--
Currently NPM only supports DNS challenge providers for which a certbot plugin exists.
You can visit pypi.org, and search for a package with the name `certbot-dns-<privider>`.
-->
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
<!--
Are you in the right place?
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*
-->
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
================================================
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
================================================
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.14.0-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>
</p>
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.
<a href="https://www.buymeacoffee.com/jc21" target="_blank"><img src="http://public.jc21.com/github/by-me-a-coffee.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
## 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 <plugin-name> --<plugin-name>-credentials <FILE> --<plugin-name>-propagation-seconds <number>
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 = <identifier>\ndns_active24_secret = <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=\"<token>\"",
"full_plugin_name": "dns-isset"
},
"joker": {
"name": "Joker",
"package_name": "certbot-dns-joker",
"version": "~=1.1.0",
"dependencies": "",
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
"full_plugin_name": "dns-joker"
},
"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 = [<blank>|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=<insert obtained API token here>",
"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=<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 = <api_key>\ndns_websupport_secret_key = <secret>",
"full_plugin_name": "dns-websupport"
},
"wedos": {
"name": "Wedos",
"package_name": "certbot-dns-wedos",
"version": "~=2.2",
"dependencies": "",
"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_password>",
"full_plugin_name": "dns-wedos"
},
"edgedns": {
"name": "Akamai Edge DNS",
"package_name": "certbot-plugin-edgedns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf",
"full_plugin_name": "edgedns"
},
"zoneedit": {
"name": "ZoneEdit",
"package_name": "certbot-dns-zoneedit",
"version": "~=0.3.2",
"dependencies": "--no-deps dnspython",
"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>",
"full_plugin_name": "dns-zoneedit"
}
}
================================================
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<boolean>}
*/
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<void>}
*/
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<boolean>}
*/
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 --<name>-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("nam
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
SYMBOL INDEX (437 symbols across 177 files)
FILE: backend/index.js
constant IP_RANGES_FETCH_ENABLED (line 11) | const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== ...
function appStart (line 13) | async function appStart() {
FILE: backend/internal/2fa.js
constant APP_NAME (line 8) | const APP_NAME = "Nginx Proxy Manager";
constant BACKUP_CODE_COUNT (line 9) | const BACKUP_CODE_COUNT = 8;
FILE: backend/internal/ip_ranges.js
constant CLOUDFRONT_URL (line 14) | const CLOUDFRONT_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
constant CLOUDFARE_V4_URL (line 15) | const CLOUDFARE_V4_URL = "https://www.cloudflare.com/ips-v4";
constant CLOUDFARE_V6_URL (line 16) | const CLOUDFARE_V6_URL = "https://www.cloudflare.com/ips-v6";
FILE: backend/internal/remote-version.js
constant VERSION_URL (line 6) | const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/ngin...
FILE: backend/internal/token.js
constant ERROR_MESSAGE_INVALID_AUTH (line 9) | const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
constant ERROR_MESSAGE_INVALID_AUTH_I18N (line 10) | const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
constant ERROR_MESSAGE_INVALID_2FA (line 11) | const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
constant ERROR_MESSAGE_INVALID_2FA_I18N (line 12) | const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
FILE: backend/internal/user.js
constant DEFAULT_AVATAR (line 15) | const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm"...
FILE: backend/lib/certbot.js
constant CERTBOT_VERSION_REPLACEMENT (line 7) | const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-...
FILE: backend/migrations/20211108145214_regenerate_default_host.js
function regenerateDefaultHost (line 6) | async function regenerateDefaultHost(knex) {
FILE: backend/models/access_list.js
class AccessList (line 17) | class AccessList extends Model {
method $beforeInsert (line 18) | $beforeInsert() {
method $beforeUpdate (line 28) | $beforeUpdate() {
method $parseDatabaseJson (line 32) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 37) | $formatDatabaseJson(json) {
method name (line 42) | static get name() {
method tableName (line 46) | static get tableName() {
method jsonAttributes (line 50) | static get jsonAttributes() {
method relationMappings (line 54) | static get relationMappings() {
FILE: backend/models/access_list_auth.js
class AccessListAuth (line 11) | class AccessListAuth extends Model {
method $beforeInsert (line 12) | $beforeInsert() {
method $beforeUpdate (line 22) | $beforeUpdate() {
method name (line 26) | static get name() {
method tableName (line 30) | static get tableName() {
method jsonAttributes (line 34) | static get jsonAttributes() {
method relationMappings (line 38) | static get relationMappings() {
FILE: backend/models/access_list_client.js
class AccessListClient (line 11) | class AccessListClient extends Model {
method $beforeInsert (line 12) | $beforeInsert() {
method $beforeUpdate (line 22) | $beforeUpdate() {
method name (line 26) | static get name() {
method tableName (line 30) | static get tableName() {
method jsonAttributes (line 34) | static get jsonAttributes() {
method relationMappings (line 38) | static get relationMappings() {
FILE: backend/models/audit-log.js
class AuditLog (line 11) | class AuditLog extends Model {
method $beforeInsert (line 12) | $beforeInsert() {
method $beforeUpdate (line 22) | $beforeUpdate() {
method name (line 26) | static get name() {
method tableName (line 30) | static get tableName() {
method jsonAttributes (line 34) | static get jsonAttributes() {
method relationMappings (line 38) | static get relationMappings() {
FILE: backend/models/auth.js
function encryptPassword (line 15) | function encryptPassword() {
class Auth (line 25) | class Auth extends Model {
method $beforeInsert (line 26) | $beforeInsert(queryContext) {
method $beforeUpdate (line 38) | $beforeUpdate(queryContext) {
method $parseDatabaseJson (line 43) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 48) | $formatDatabaseJson(json) {
method verifyPassword (line 59) | verifyPassword(password) {
method name (line 63) | static get name() {
method tableName (line 67) | static get tableName() {
method jsonAttributes (line 71) | static get jsonAttributes() {
method relationMappings (line 75) | static get relationMappings() {
FILE: backend/models/certificate.js
class Certificate (line 18) | class Certificate extends Model {
method $beforeInsert (line 19) | $beforeInsert() {
method $beforeUpdate (line 41) | $beforeUpdate() {
method $parseDatabaseJson (line 50) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 55) | $formatDatabaseJson(json) {
method name (line 60) | static get name() {
method tableName (line 64) | static get tableName() {
method jsonAttributes (line 68) | static get jsonAttributes() {
method relationMappings (line 72) | static get relationMappings() {
FILE: backend/models/dead_host.js
class DeadHost (line 15) | class DeadHost extends Model {
method $beforeInsert (line 16) | $beforeInsert() {
method $beforeUpdate (line 33) | $beforeUpdate() {
method $parseDatabaseJson (line 42) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 47) | $formatDatabaseJson(json) {
method name (line 52) | static get name() {
method tableName (line 56) | static get tableName() {
method jsonAttributes (line 60) | static get jsonAttributes() {
method defaultAllowGraph (line 64) | static get defaultAllowGraph() {
method defaultExpand (line 68) | static get defaultExpand() {
method defaultOrder (line 72) | static get defaultOrder() {
method relationMappings (line 76) | static get relationMappings() {
FILE: backend/models/proxy_host.js
class ProxyHost (line 27) | class ProxyHost extends Model {
method $beforeInsert (line 28) | $beforeInsert() {
method $beforeUpdate (line 45) | $beforeUpdate() {
method $parseDatabaseJson (line 54) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 59) | $formatDatabaseJson(json) {
method name (line 64) | static get name() {
method tableName (line 68) | static get tableName() {
method jsonAttributes (line 72) | static get jsonAttributes() {
method defaultAllowGraph (line 76) | static get defaultAllowGraph() {
method defaultExpand (line 80) | static get defaultExpand() {
method defaultOrder (line 84) | static get defaultOrder() {
method relationMappings (line 88) | static get relationMappings() {
FILE: backend/models/redirection_host.js
class RedirectionHost (line 24) | class RedirectionHost extends Model {
method $beforeInsert (line 25) | $beforeInsert() {
method $beforeUpdate (line 42) | $beforeUpdate() {
method $parseDatabaseJson (line 51) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 56) | $formatDatabaseJson(json) {
method name (line 61) | static get name() {
method tableName (line 65) | static get tableName() {
method jsonAttributes (line 69) | static get jsonAttributes() {
method defaultAllowGraph (line 73) | static get defaultAllowGraph() {
method defaultExpand (line 77) | static get defaultExpand() {
method defaultOrder (line 81) | static get defaultOrder() {
method relationMappings (line 85) | static get relationMappings() {
FILE: backend/models/setting.js
class Setting (line 9) | class Setting extends Model {
method $beforeInsert (line 10) | $beforeInsert () {
method name (line 17) | static get name () {
method tableName (line 21) | static get tableName () {
method jsonAttributes (line 25) | static get jsonAttributes () {
FILE: backend/models/stream.js
class Stream (line 12) | class Stream extends Model {
method $beforeInsert (line 13) | $beforeInsert() {
method $beforeUpdate (line 23) | $beforeUpdate() {
method $parseDatabaseJson (line 27) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 32) | $formatDatabaseJson(json) {
method name (line 37) | static get name() {
method tableName (line 41) | static get tableName() {
method jsonAttributes (line 45) | static get jsonAttributes() {
method defaultAllowGraph (line 49) | static get defaultAllowGraph() {
method defaultExpand (line 53) | static get defaultExpand() {
method defaultOrder (line 57) | static get defaultOrder() {
method relationMappings (line 61) | static get relationMappings() {
FILE: backend/models/token.js
constant ALGO (line 13) | const ALGO = "RS256";
FILE: backend/models/user.js
class User (line 14) | class User extends Model {
method $beforeInsert (line 15) | $beforeInsert() {
method $beforeUpdate (line 25) | $beforeUpdate() {
method $parseDatabaseJson (line 29) | $parseDatabaseJson(json) {
method $formatDatabaseJson (line 34) | $formatDatabaseJson(json) {
method name (line 39) | static get name() {
method tableName (line 43) | static get tableName() {
method jsonAttributes (line 47) | static get jsonAttributes() {
method relationMappings (line 51) | static get relationMappings() {
FILE: backend/models/user_permission.js
class UserPermission (line 10) | class UserPermission extends Model {
method $beforeInsert (line 11) | $beforeInsert () {
method $beforeUpdate (line 16) | $beforeUpdate () {
method name (line 20) | static get name () {
method tableName (line 24) | static get tableName () {
FILE: docker/dev/pdns-db.sql
type `comments` (line 47) | CREATE TABLE `comments` (
type `cryptokeys` (line 77) | CREATE TABLE `cryptokeys` (
type `domainmetadata` (line 105) | CREATE TABLE `domainmetadata` (
type `domains` (line 133) | CREATE TABLE `domains` (
type `records` (line 167) | CREATE TABLE `records` (
type `supermasters` (line 205) | CREATE TABLE `supermasters` (
type `tsigkeys` (line 229) | CREATE TABLE `tsigkeys` (
FILE: frontend/check-locales.cjs
constant BACKEND_ERRORS_FILE (line 41) | const BACKEND_ERRORS_FILE = "../backend/internal/errors/errors.go";
constant BACKEND_ERRORS (line 42) | const BACKEND_ERRORS = [];
FILE: frontend/src/App.tsx
function App (line 13) | function App() {
FILE: frontend/src/Router.tsx
function Router (line 29) | function Router() {
FILE: frontend/src/api/backend/base.ts
type BuildUrlArgs (line 9) | interface BuildUrlArgs {
function decamelizeParams (line 14) | function decamelizeParams(params?: StringifiableRecord): StringifiableRe...
function buildUrl (line 26) | function buildUrl({ url, params }: BuildUrlArgs) {
function buildAuthHeader (line 36) | function buildAuthHeader(): Record<string, string> | undefined {
function buildBody (line 43) | function buildBody(data?: Record<string, any>): string | undefined {
function processResponse (line 49) | async function processResponse(response: Response) {
type GetArgs (line 65) | interface GetArgs {
function baseGet (line 70) | async function baseGet({ url, params }: GetArgs, abortController?: Abort...
function get (line 79) | async function get(args: GetArgs, abortController?: AbortController) {
function download (line 83) | async function download({ url, params }: GetArgs, filename = "download.f...
type PostArgs (line 95) | interface PostArgs {
function post (line 102) | async function post({ url, params, data, noAuth }: PostArgs, abortContro...
type PutArgs (line 132) | interface PutArgs {
function put (line 137) | async function put({ url, params, data }: PutArgs, abortController?: Abo...
type DeleteArgs (line 150) | interface DeleteArgs {
function del (line 154) | async function del({ url, params }: DeleteArgs, abortController?: AbortC...
FILE: frontend/src/api/backend/checkVersion.ts
function checkVersion (line 4) | async function checkVersion(): Promise<VersionCheckResponse> {
FILE: frontend/src/api/backend/createAccessList.ts
function createAccessList (line 4) | async function createAccessList(item: AccessList): Promise<AccessList> {
FILE: frontend/src/api/backend/createCertificate.ts
function createCertificate (line 4) | async function createCertificate(item: Certificate): Promise<Certificate> {
FILE: frontend/src/api/backend/createDeadHost.ts
function createDeadHost (line 4) | async function createDeadHost(item: DeadHost): Promise<DeadHost> {
FILE: frontend/src/api/backend/createProxyHost.ts
function createProxyHost (line 4) | async function createProxyHost(item: ProxyHost): Promise<ProxyHost> {
FILE: frontend/src/api/backend/createRedirectionHost.ts
function createRedirectionHost (line 4) | async function createRedirectionHost(item: RedirectionHost): Promise<Red...
FILE: frontend/src/api/backend/createStream.ts
function createStream (line 4) | async function createStream(item: Stream): Promise<Stream> {
FILE: frontend/src/api/backend/createUser.ts
type AuthOptions (line 4) | interface AuthOptions {
type NewUser (line 9) | interface NewUser {
function createUser (line 18) | async function createUser(item: NewUser, noAuth?: boolean): Promise<User> {
FILE: frontend/src/api/backend/deleteAccessList.ts
function deleteAccessList (line 3) | async function deleteAccessList(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteCertificate.ts
function deleteCertificate (line 3) | async function deleteCertificate(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteDeadHost.ts
function deleteDeadHost (line 3) | async function deleteDeadHost(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteProxyHost.ts
function deleteProxyHost (line 3) | async function deleteProxyHost(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteRedirectionHost.ts
function deleteRedirectionHost (line 3) | async function deleteRedirectionHost(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteStream.ts
function deleteStream (line 3) | async function deleteStream(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/deleteUser.ts
function deleteUser (line 3) | async function deleteUser(id: number): Promise<boolean> {
FILE: frontend/src/api/backend/downloadCertificate.ts
function downloadCertificate (line 3) | async function downloadCertificate(id: number): Promise<void> {
FILE: frontend/src/api/backend/expansions.ts
type AccessListExpansion (line 1) | type AccessListExpansion = "owner" | "items" | "clients";
type AuditLogExpansion (line 2) | type AuditLogExpansion = "user";
type CertificateExpansion (line 3) | type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts...
type HostExpansion (line 4) | type HostExpansion = "owner" | "certificate";
type ProxyHostExpansion (line 5) | type ProxyHostExpansion = "owner" | "access_list" | "certificate";
type UserExpansion (line 6) | type UserExpansion = "permissions";
FILE: frontend/src/api/backend/getAccessList.ts
function getAccessList (line 5) | async function getAccessList(id: number, expand?: AccessListExpansion[],...
FILE: frontend/src/api/backend/getAccessLists.ts
function getAccessLists (line 5) | async function getAccessLists(expand?: AccessListExpansion[], params = {...
FILE: frontend/src/api/backend/getAuditLog.ts
function getAuditLog (line 5) | async function getAuditLog(id: number, expand?: AuditLogExpansion[], par...
FILE: frontend/src/api/backend/getAuditLogs.ts
function getAuditLogs (line 5) | async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): ...
FILE: frontend/src/api/backend/getCertificate.ts
function getCertificate (line 5) | async function getCertificate(id: number, expand?: CertificateExpansion[...
FILE: frontend/src/api/backend/getCertificateDNSProviders.ts
function getCertificateDNSProviders (line 4) | async function getCertificateDNSProviders(params = {}): Promise<DNSProvi...
FILE: frontend/src/api/backend/getCertificates.ts
function getCertificates (line 5) | async function getCertificates(expand?: CertificateExpansion[], params =...
FILE: frontend/src/api/backend/getDeadHost.ts
function getDeadHost (line 5) | async function getDeadHost(id: number, expand?: HostExpansion[], params ...
FILE: frontend/src/api/backend/getDeadHosts.ts
function getDeadHosts (line 5) | async function getDeadHosts(expand?: HostExpansion[], params = {}): Prom...
FILE: frontend/src/api/backend/getHealth.ts
function getHealth (line 4) | async function getHealth(): Promise<HealthResponse> {
FILE: frontend/src/api/backend/getHostsReport.ts
function getHostsReport (line 3) | async function getHostsReport(): Promise<Record<string, number>> {
FILE: frontend/src/api/backend/getProxyHost.ts
function getProxyHost (line 5) | async function getProxyHost(id: number, expand?: ProxyHostExpansion[], p...
FILE: frontend/src/api/backend/getProxyHosts.ts
function getProxyHosts (line 5) | async function getProxyHosts(expand?: ProxyHostExpansion[], params = {})...
FILE: frontend/src/api/backend/getRedirectionHost.ts
function getRedirectionHost (line 5) | async function getRedirectionHost(id: number, expand?: HostExpansion[], ...
FILE: frontend/src/api/backend/getRedirectionHosts.ts
function getRedirectionHosts (line 5) | async function getRedirectionHosts(expand?: HostExpansion[], params = {}...
FILE: frontend/src/api/backend/getSetting.ts
function getSetting (line 4) | async function getSetting(id: string, expand?: string[], params = {}): P...
FILE: frontend/src/api/backend/getSettings.ts
function getSettings (line 4) | async function getSettings(expand?: string[], params = {}): Promise<Sett...
FILE: frontend/src/api/backend/getStream.ts
function getStream (line 5) | async function getStream(id: number, expand?: HostExpansion[], params = ...
FILE: frontend/src/api/backend/getStreams.ts
function getStreams (line 5) | async function getStreams(expand?: HostExpansion[], params = {}): Promis...
FILE: frontend/src/api/backend/getToken.ts
type LoginResponse (line 4) | type LoginResponse = TokenResponse | TwoFactorChallengeResponse;
function isTwoFactorChallenge (line 6) | function isTwoFactorChallenge(response: LoginResponse): response is TwoF...
function getToken (line 10) | async function getToken(identity: string, secret: string): Promise<Login...
function verify2FA (line 17) | async function verify2FA(challengeToken: string, code: string): Promise<...
FILE: frontend/src/api/backend/getUser.ts
function getUser (line 5) | async function getUser(id: number | string = "me", expand?: UserExpansio...
FILE: frontend/src/api/backend/getUsers.ts
function getUsers (line 5) | async function getUsers(expand?: UserExpansion[], params = {}): Promise<...
FILE: frontend/src/api/backend/helpers.ts
function tableSortToAPI (line 8) | function tableSortToAPI(sortBy: any): string | undefined {
function tableFiltersToAPI (line 25) | function tableFiltersToAPI(filters: any[]): { [key: string]: string } {
function buildFilters (line 40) | function buildFilters(filters?: Record<string, string | boolean | undefi...
FILE: frontend/src/api/backend/loginAsUser.ts
function loginAsUser (line 4) | async function loginAsUser(id: number): Promise<LoginAsTokenResponse> {
FILE: frontend/src/api/backend/models.ts
type AppVersion (line 1) | interface AppVersion {
type UserPermissions (line 7) | interface UserPermissions {
type User (line 21) | interface User {
type AuditLog (line 34) | interface AuditLog {
type AccessList (line 47) | interface AccessList {
type AccessListItem (line 63) | interface AccessListItem {
type AccessListClient (line 74) | type AccessListClient = {
type Certificate (line 84) | interface Certificate {
type ProxyLocation (line 100) | interface ProxyLocation {
type ProxyHost (line 108) | interface ProxyHost {
type DeadHost (line 137) | interface DeadHost {
type RedirectionHost (line 156) | interface RedirectionHost {
type Stream (line 180) | interface Stream {
type Setting (line 198) | interface Setting {
type DNSProvider (line 206) | interface DNSProvider {
FILE: frontend/src/api/backend/refreshToken.ts
function refreshToken (line 4) | async function refreshToken(): Promise<TokenResponse> {
FILE: frontend/src/api/backend/renewCertificate.ts
function renewCertificate (line 4) | async function renewCertificate(id: number): Promise<Certificate> {
FILE: frontend/src/api/backend/responseTypes.ts
type HealthResponse (line 3) | interface HealthResponse {
type TokenResponse (line 9) | interface TokenResponse {
type ValidatedCertificateResponse (line 14) | interface ValidatedCertificateResponse {
type LoginAsTokenResponse (line 19) | interface LoginAsTokenResponse extends TokenResponse {
type VersionCheckResponse (line 23) | interface VersionCheckResponse {
type TwoFactorChallengeResponse (line 29) | interface TwoFactorChallengeResponse {
type TwoFactorStatusResponse (line 34) | interface TwoFactorStatusResponse {
type TwoFactorSetupResponse (line 39) | interface TwoFactorSetupResponse {
type TwoFactorEnableResponse (line 44) | interface TwoFactorEnableResponse {
FILE: frontend/src/api/backend/setPermissions.ts
function setPermissions (line 4) | async function setPermissions(userId: number, data: UserPermissions): Pr...
FILE: frontend/src/api/backend/testHttpCertificate.ts
function testHttpCertificate (line 3) | async function testHttpCertificate(domains: string[]): Promise<Record<st...
FILE: frontend/src/api/backend/toggleDeadHost.ts
function toggleDeadHost (line 3) | async function toggleDeadHost(id: number, enabled: boolean): Promise<boo...
FILE: frontend/src/api/backend/toggleProxyHost.ts
function toggleProxyHost (line 3) | async function toggleProxyHost(id: number, enabled: boolean): Promise<bo...
FILE: frontend/src/api/backend/toggleRedirectionHost.ts
function toggleRedirectionHost (line 3) | async function toggleRedirectionHost(id: number, enabled: boolean): Prom...
FILE: frontend/src/api/backend/toggleStream.ts
function toggleStream (line 3) | async function toggleStream(id: number, enabled: boolean): Promise<boole...
FILE: frontend/src/api/backend/toggleUser.ts
function toggleUser (line 4) | async function toggleUser(id: number, enabled: boolean): Promise<boolean> {
FILE: frontend/src/api/backend/twoFactor.ts
function get2FAStatus (line 4) | async function get2FAStatus(userId: number | "me"): Promise<TwoFactorSta...
function start2FASetup (line 10) | async function start2FASetup(userId: number | "me"): Promise<TwoFactorSe...
function enable2FA (line 16) | async function enable2FA(userId: number | "me", code: string): Promise<T...
function disable2FA (line 23) | async function disable2FA(userId: number | "me", code: string): Promise<...
function regenerateBackupCodes (line 32) | async function regenerateBackupCodes(userId: number | "me", code: string...
FILE: frontend/src/api/backend/updateAccessList.ts
function updateAccessList (line 4) | async function updateAccessList(item: AccessList): Promise<AccessList> {
FILE: frontend/src/api/backend/updateAuth.ts
function updateAuth (line 4) | async function updateAuth(userId: number | "me", newPassword: string, cu...
FILE: frontend/src/api/backend/updateDeadHost.ts
function updateDeadHost (line 4) | async function updateDeadHost(item: DeadHost): Promise<DeadHost> {
FILE: frontend/src/api/backend/updateProxyHost.ts
function updateProxyHost (line 4) | async function updateProxyHost(item: ProxyHost): Promise<ProxyHost> {
FILE: frontend/src/api/backend/updateRedirectionHost.ts
function updateRedirectionHost (line 4) | async function updateRedirectionHost(item: RedirectionHost): Promise<Red...
FILE: frontend/src/api/backend/updateSetting.ts
function updateSetting (line 4) | async function updateSetting(item: Setting): Promise<Setting> {
FILE: frontend/src/api/backend/updateStream.ts
function updateStream (line 4) | async function updateStream(item: Stream): Promise<Stream> {
FILE: frontend/src/api/backend/updateUser.ts
function updateUser (line 4) | async function updateUser(item: User): Promise<User> {
FILE: frontend/src/api/backend/uploadCertificate.ts
function uploadCertificate (line 4) | async function uploadCertificate(id: number, data: FormData): Promise<Ce...
FILE: frontend/src/api/backend/validateCertificate.ts
function validateCertificate (line 4) | async function validateCertificate(data: FormData): Promise<ValidatedCer...
FILE: frontend/src/components/Button.tsx
type Props (line 4) | interface Props {
function Button (line 29) | function Button({
FILE: frontend/src/components/EmptyData.tsx
type Props (line 8) | interface Props {
function EmptyData (line 19) | function EmptyData({
FILE: frontend/src/components/ErrorNotFound.tsx
function ErrorNotFound (line 5) | function ErrorNotFound() {
FILE: frontend/src/components/Flag.tsx
type FlagProps (line 6) | interface FlagProps {
function Flag (line 10) | function Flag({ className, countryCode }: FlagProps) {
FILE: frontend/src/components/Form/AccessClientFields.tsx
type Props (line 8) | interface Props {
function AccessClientFields (line 12) | function AccessClientFields({ initialValues, name = "clients" }: Props) {
FILE: frontend/src/components/Form/AccessField.tsx
type AccessOption (line 10) | interface AccessOption {
type Props (line 30) | interface Props {
function AccessField (line 35) | function AccessField({ name = "accessListId", label = "access-list", id ...
FILE: frontend/src/components/Form/BasicAuthFields.tsx
type Props (line 7) | interface Props {
function BasicAuthFields (line 11) | function BasicAuthFields({ initialValues, name = "items" }: Props) {
FILE: frontend/src/components/Form/DNSProviderFields.tsx
type DNSProviderOption (line 11) | interface DNSProviderOption {
type Props (line 17) | interface Props {
function DNSProviderFields (line 20) | function DNSProviderFields({ showBoundaryBox = false }: Props) {
FILE: frontend/src/components/Form/DomainNamesField.tsx
type SelectOption (line 8) | type SelectOption = {
type Props (line 14) | interface Props {
function DomainNamesField (line 23) | function DomainNamesField({
FILE: frontend/src/components/Form/LocationsFields.tsx
type Props (line 10) | interface Props {
function LocationsFields (line 14) | function LocationsFields({ initialValues, name = "locations" }: Props) {
FILE: frontend/src/components/Form/NginxConfigField.tsx
type Props (line 5) | interface Props {
function NginxConfigField (line 10) | function NginxConfigField({
FILE: frontend/src/components/Form/SSLCertificateField.tsx
type CertOption (line 9) | interface CertOption {
type Props (line 29) | interface Props {
function SSLCertificateField (line 37) | function SSLCertificateField({
FILE: frontend/src/components/Form/SSLOptionsFields.tsx
type Props (line 6) | interface Props {
function SSLOptionsFields (line 13) | function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceD...
FILE: frontend/src/components/HasPermission.tsx
type Props (line 8) | interface Props {
function HasPermission (line 16) | function HasPermission({
FILE: frontend/src/components/Loading.tsx
type Props (line 5) | interface Props {
function Loading (line 9) | function Loading({ label, noLogo }: Props) {
FILE: frontend/src/components/LoadingPage.tsx
type Props (line 3) | interface Props {
function LoadingPage (line 7) | function LoadingPage({ label, noLogo }: Props) {
FILE: frontend/src/components/LocalePicker.tsx
type Props (line 8) | interface Props {
function LocalePicker (line 12) | function LocalePicker({ menuAlign = "start" }: Props) {
FILE: frontend/src/components/NavLink.tsx
type Props (line 3) | interface Props {
function NavLink (line 9) | function NavLink({ children, to, isDropdownItem, onClick }: Props) {
FILE: frontend/src/components/Page.tsx
type Props (line 4) | interface Props {
function Page (line 8) | function Page({ children, className }: Props) {
FILE: frontend/src/components/SiteContainer.tsx
type Props (line 1) | interface Props {
function SiteContainer (line 4) | function SiteContainer({ children }: Props) {
FILE: frontend/src/components/SiteFooter.tsx
function SiteFooter (line 4) | function SiteFooter() {
FILE: frontend/src/components/SiteHeader.tsx
function SiteHeader (line 9) | function SiteHeader() {
FILE: frontend/src/components/SiteMenu.tsx
type MenuItem (line 27) | interface MenuItem {
function SiteMenu (line 178) | function SiteMenu() {
FILE: frontend/src/components/Table/EmptyRow.tsx
type Props (line 3) | interface Props {
function EmptyRow (line 6) | function EmptyRow({ tableInstance }: Props) {
FILE: frontend/src/components/Table/Formatter/AccessListformatter.tsx
type Props (line 5) | interface Props {
function AccessListFormatter (line 8) | function AccessListFormatter({ access }: Props) {
FILE: frontend/src/components/Table/Formatter/CertificateFormatter.tsx
type Props (line 4) | interface Props {
function CertificateFormatter (line 7) | function CertificateFormatter({ certificate }: Props) {
FILE: frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx
type Props (line 47) | interface Props {
function CertificateInUseFormatter (line 53) | function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadH...
FILE: frontend/src/components/Table/Formatter/DateFormatter.tsx
type Props (line 6) | interface Props {
function DateFormatter (line 11) | function DateFormatter({ value, highlightPast, highlistNearlyExpired }: ...
FILE: frontend/src/components/Table/Formatter/DomainsFormatter.tsx
type Props (line 6) | interface Props {
function DomainsFormatter (line 40) | function DomainsFormatter({ domains, createdOn, niceName, provider, colo...
FILE: frontend/src/components/Table/Formatter/EmailFormatter.tsx
type Props (line 1) | interface Props {
function EmailFormatter (line 4) | function EmailFormatter({ email }: Props) {
FILE: frontend/src/components/Table/Formatter/EventFormatter.tsx
type Props (line 66) | interface Props {
function EventFormatter (line 69) | function EventFormatter({ row }: Props) {
FILE: frontend/src/components/Table/Formatter/GravatarFormatter.tsx
type Props (line 3) | interface Props {
function GravatarFormatter (line 7) | function GravatarFormatter({ url, name }: Props) {
FILE: frontend/src/components/Table/Formatter/RolesFormatter.tsx
type Props (line 3) | interface Props {
function RolesFormatter (line 6) | function RolesFormatter({ roles }: Props) {
FILE: frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx
type Props (line 4) | interface Props {
function TrueFalseFormatter (line 11) | function TrueFalseFormatter({
FILE: frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx
type Props (line 4) | interface Props {
function ValueWithDateFormatter (line 9) | function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
FILE: frontend/src/components/Table/TableBody.tsx
function TableBody (line 5) | function TableBody<T>(props: TableLayoutProps<T>) {
FILE: frontend/src/components/Table/TableHeader.tsx
function TableHeader (line 3) | function TableHeader<T>(props: TableLayoutProps<T>) {
FILE: frontend/src/components/Table/TableHelpers.ts
type TablePagination (line 1) | interface TablePagination {
type TableSortBy (line 7) | interface TableSortBy {
type TableFilter (line 12) | interface TableFilter {
FILE: frontend/src/components/Table/TableLayout.tsx
type TableLayoutProps (line 5) | interface TableLayoutProps<TFields> {
function TableLayout (line 12) | function TableLayout<TFields>(props: TableLayoutProps<TFields>) {
FILE: frontend/src/components/ThemeSwitcher.tsx
type Props (line 7) | interface Props {
function ThemeSwitcher (line 10) | function ThemeSwitcher({ className }: Props) {
FILE: frontend/src/components/Unhealthy.tsx
function Unhealthy (line 3) | function Unhealthy() {
FILE: frontend/src/context/AuthContext.tsx
type TwoFactorChallenge (line 15) | interface TwoFactorChallenge {
type AuthContextType (line 20) | interface AuthContextType {
type Props (line 35) | interface Props {
function AuthProvider (line 39) | function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }...
function useAuthState (line 118) | function useAuthState() {
FILE: frontend/src/context/LocaleContext.tsx
type LocaleContextType (line 5) | interface LocaleContextType {
type Props (line 14) | interface Props {
function LocaleProvider (line 17) | function LocaleProvider({ children }: Props) {
function useLocaleState (line 29) | function useLocaleState() {
FILE: frontend/src/context/ThemeContext.tsx
type Theme (line 9) | type Theme = "light" | "dark";
type ThemeContextType (line 11) | interface ThemeContextType {
type ThemeProviderProps (line 20) | interface ThemeProviderProps {
function useTheme (line 64) | function useTheme(): ThemeContextType {
FILE: frontend/src/locale/Utils.test.tsx
method constructor (line 20) | constructor(_locales?: string | string[], options?: Intl.DateTimeFormatO...
FILE: frontend/src/locale/scripts/locale-sort.cjs
constant DIR (line 6) | const DIR = path.resolve(__dirname, "../src");
function sortKeys (line 9) | function sortKeys(obj) {
FILE: frontend/src/modals/AccessListModal.tsx
type Props (line 18) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/ChangePasswordModal.tsx
type Props (line 15) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/DeadHostModal.tsx
type Props (line 23) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/DeleteConfirmModal.tsx
type ShowProps (line 9) | interface ShowProps {
type Props (line 17) | interface Props extends InnerModalProps, ShowProps {}
FILE: frontend/src/modals/EventDetailsModal.tsx
type Props (line 13) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/HelpModal.tsx
type Props (line 10) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/PermissionsModal.tsx
type Props (line 18) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/ProxyHostModal.tsx
type Props (line 29) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/RedirectionHostModal.tsx
type Props (line 25) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/RenewCertificateModal.tsx
type Props (line 12) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/SetPasswordModal.tsx
type Props (line 16) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/StreamModal.tsx
type Props (line 16) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/TwoFactorModal.tsx
type Step (line 17) | type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disa...
type Props (line 23) | interface Props extends InnerModalProps {
FILE: frontend/src/modals/UserModal.tsx
type Props (line 16) | interface Props extends InnerModalProps {
FILE: frontend/src/modules/AuthStore.ts
constant TOKEN_KEY (line 4) | const TOKEN_KEY = "authentications";
class AuthStore (line 6) | class AuthStore {
method tokens (line 8) | get tokens() {
method token (line 22) | get token() {
method expires (line 31) | get expires() {
method hasActiveToken (line 48) | hasActiveToken() {
method set (line 68) | set({ token, expires }: TokenResponse) {
method add (line 73) | add({ token, expires }: TokenResponse) {
method drop (line 80) | drop() {
method clear (line 86) | clear() {
method count (line 90) | count() {
FILE: frontend/src/modules/Permissions.ts
constant ADMIN (line 3) | const ADMIN = "admin";
constant VISIBILITY (line 4) | const VISIBILITY = "visibility";
constant PROXY_HOSTS (line 5) | const PROXY_HOSTS = "proxyHosts";
constant REDIRECTION_HOSTS (line 6) | const REDIRECTION_HOSTS = "redirectionHosts";
constant DEAD_HOSTS (line 7) | const DEAD_HOSTS = "deadHosts";
constant STREAMS (line 8) | const STREAMS = "streams";
constant CERTIFICATES (line 9) | const CERTIFICATES = "certificates";
constant ACCESS_LISTS (line 10) | const ACCESS_LISTS = "accessLists";
constant MANAGE (line 12) | const MANAGE = "manage";
constant VIEW (line 13) | const VIEW = "view";
constant HIDDEN (line 14) | const HIDDEN = "hidden";
constant ALL (line 16) | const ALL = "all";
constant USER (line 17) | const USER = "user";
type Section (line 19) | type Section =
type Permission (line 29) | type Permission = typeof MANAGE | typeof VIEW;
FILE: frontend/src/notifications/Msg.tsx
function Msg (line 5) | function Msg({ data }: any) {
FILE: frontend/src/pages/Access/Table.tsx
type Props (line 10) | interface Props {
function Table (line 18) | function Table({ data, isFetching, isFiltered, onEdit, onDelete, onNew }...
FILE: frontend/src/pages/Access/TableWrapper.tsx
function TableWrapper (line 13) | function TableWrapper() {
FILE: frontend/src/pages/AuditLog/Table.tsx
type Props (line 8) | interface Props {
function Table (line 13) | function Table({ data, isFetching, onSelectItem }: Props) {
FILE: frontend/src/pages/AuditLog/TableWrapper.tsx
function TableWrapper (line 8) | function TableWrapper() {
FILE: frontend/src/pages/Certificates/Table.tsx
type Props (line 18) | interface Props {
function Table (line 26) | function Table({ data, isFetching, onDelete, onRenew, onDownload, isFilt...
FILE: frontend/src/pages/Certificates/TableWrapper.tsx
function TableWrapper (line 20) | function TableWrapper() {
FILE: frontend/src/pages/Login/index.tsx
function TwoFactorForm (line 11) | function TwoFactorForm() {
function LoginForm (line 80) | function LoginForm() {
function Login (line 169) | function Login() {
FILE: frontend/src/pages/Nginx/DeadHosts/Table.tsx
type Props (line 17) | interface Props {
function Table (line 26) | function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, on...
FILE: frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx
function TableWrapper (line 14) | function TableWrapper() {
FILE: frontend/src/pages/Nginx/ProxyHosts/Table.tsx
type Props (line 18) | interface Props {
function Table (line 27) | function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, on...
FILE: frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx
function TableWrapper (line 14) | function TableWrapper() {
FILE: frontend/src/pages/Nginx/RedirectionHosts/Table.tsx
type Props (line 17) | interface Props {
function Table (line 26) | function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, on...
FILE: frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx
function TableWrapper (line 14) | function TableWrapper() {
FILE: frontend/src/pages/Nginx/Streams/Table.tsx
type Props (line 17) | interface Props {
function Table (line 26) | function Table({ data, isFetching, isFiltered, onEdit, onDelete, onDisab...
FILE: frontend/src/pages/Nginx/Streams/TableWrapper.tsx
function TableWrapper (line 14) | function TableWrapper() {
FILE: frontend/src/pages/Settings/DefaultSite.tsx
function DefaultSite (line 11) | function DefaultSite() {
FILE: frontend/src/pages/Settings/Layout.tsx
function Layout (line 4) | function Layout() {
FILE: frontend/src/pages/Setup/index.tsx
type Payload (line 13) | interface Payload {
function Setup (line 19) | function Setup() {
FILE: frontend/src/pages/Users/Table.tsx
type Props (line 24) | interface Props {
function Table (line 37) | function Table({
FILE: frontend/src/pages/Users/TableWrapper.tsx
function TableWrapper (line 14) | function TableWrapper() {
FILE: frontend/vite.config.ts
method configureServer (line 28) | configureServer(_server) {
method configureServer (line 34) | configureServer(server) {
FILE: test/cypress/config/ci.mjs
method setupNodeEvents (line 15) | setupNodeEvents(on, config) {
FILE: test/cypress/config/dev.mjs
method setupNodeEvents (line 15) | setupNodeEvents(on, config) {
FILE: test/cypress/plugins/index.mjs
method log (line 18) | log(message) {
Condensed preview — 789 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,849K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1644,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n<!--\n \nAre you i"
},
{
"path": ".github/ISSUE_TEMPLATE/dns_challenge_request.md",
"chars": 509,
"preview": "---\nname: DNS challenge provider request\nabout: Suggest a new provider to be available for a certificate DNS challenge\nt"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 1066,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n<!--"
},
{
"path": ".github/dependabot.yml",
"chars": 2423,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/backend\"\n schedule:\n interval: \"weekly\"\n gr"
},
{
"path": ".github/workflows/stale.yml",
"chars": 719,
"preview": "name: 'Close stale issues and PRs'\non:\n schedule:\n - cron: '30 1 * * *'\n workflow_dispatch:\n\njobs:\n stale:\n run"
},
{
"path": ".gitignore",
"chars": 144,
"preview": ".DS_Store\n.idea\n.qodo\n._*\n.vscode\ncertbot-help.txt\ntest/node_modules\n*/node_modules\ndocker/dev/dnsrouter-config.json.tmp"
},
{
"path": ".version",
"chars": 7,
"preview": "2.14.0\n"
},
{
"path": "LICENSE",
"chars": 1057,
"preview": "MIT License\n\nCopyright (c) 2017 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this s"
},
{
"path": "README.md",
"chars": 4386,
"preview": "<p align=\"center\">\n\t<img src=\"https://nginxproxymanager.com/github.png\">\n\t<br><br>\n\t<img src=\"https://img.shields.io/bad"
},
{
"path": "backend/.gitignore",
"chars": 83,
"preview": "config/development.json\ndata/*\nyarn-error.log\ntmp\ncertbot.log\nnode_modules\ncore.*\n\n"
},
{
"path": "backend/app.js",
"chars": 2283,
"preview": "import bodyParser from \"body-parser\";\nimport compression from \"compression\";\nimport express from \"express\";\nimport fileU"
},
{
"path": "backend/biome.json",
"chars": 2648,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.4.5/schema.json\",\n \"vcs\": {\n \"enabled\": true,\n \"clientK"
},
{
"path": "backend/certbot/README.md",
"chars": 855,
"preview": "# Certbot dns-plugins\n\nThis file contains info about available Certbot DNS plugins.\nThis only works for plugins which us"
},
{
"path": "backend/certbot/dns-plugins.json",
"chars": 26124,
"preview": "{\n\t\"acmedns\": {\n\t\t\"name\": \"ACME-DNS\",\n\t\t\"package_name\": \"certbot-dns-acmedns\",\n\t\t\"version\": \"~=0.1.0\",\n\t\t\"dependencies\":"
},
{
"path": "backend/config/README.md",
"chars": 87,
"preview": "These files are use in development and are not deployed as part of the final product.\n "
},
{
"path": "backend/config/default.json",
"chars": 144,
"preview": "{\n \"database\": {\n \"engine\": \"mysql2\",\n \"host\": \"db\",\n \"name\": \"npm\",\n \"user\": \"npm\",\n \"password\": \"npm\","
},
{
"path": "backend/config/sqlite-test-db.json",
"chars": 685,
"preview": "{\n \"database\": {\n \"engine\": \"knex-native\",\n \"knex\": {\n \"client\": \"better-sqlite3\",\n \"connection"
},
{
"path": "backend/db.js",
"chars": 794,
"preview": "import knex from \"knex\";\nimport {configGet, configHas} from \"./lib/config.js\";\n\nlet instance = null;\n\nconst generateDbCo"
},
{
"path": "backend/index.js",
"chars": 1438,
"preview": "#!/usr/bin/env node\n\nimport app from \"./app.js\";\nimport internalCertificate from \"./internal/certificate.js\";\nimport int"
},
{
"path": "backend/internal/2fa.js",
"chars": 7747,
"preview": "import crypto from \"node:crypto\";\nimport bcrypt from \"bcrypt\";\nimport { createGuardrails, generateSecret, generateURI, v"
},
{
"path": "backend/internal/access-list.js",
"chars": 12515,
"preview": "import fs from \"node:fs\";\nimport batchflow from \"batchflow\";\nimport _ from \"lodash\";\nimport errs from \"../lib/error.js\";"
},
{
"path": "backend/internal/audit-log.js",
"chars": 2643,
"preview": "import errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport auditLogModel from \"../mo"
},
{
"path": "backend/internal/certificate.js",
"chars": 37995,
"preview": "import fs from \"node:fs\";\nimport https from \"node:https\";\nimport path from \"path\";\nimport archiver from \"archiver\";\nimpo"
},
{
"path": "backend/internal/dead-host.js",
"chars": 10027,
"preview": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport u"
},
{
"path": "backend/internal/host.js",
"chars": 5871,
"preview": "import _ from \"lodash\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport deadHostModel from \"../models/dead_ho"
},
{
"path": "backend/internal/ip_ranges.js",
"chars": 4401,
"preview": "import fs from \"node:fs\";\nimport https from \"node:https\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } "
},
{
"path": "backend/internal/nginx.js",
"chars": 12321,
"preview": "import fs from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport _ from \""
},
{
"path": "backend/internal/proxy-host.js",
"chars": 12053,
"preview": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport u"
},
{
"path": "backend/internal/redirection-host.js",
"chars": 12235,
"preview": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport u"
},
{
"path": "backend/internal/remote-version.js",
"chars": 2554,
"preview": "import https from \"node:https\";\nimport { ProxyAgent } from \"proxy-agent\";\nimport { debug, remoteVersion as logger } from"
},
{
"path": "backend/internal/report.js",
"chars": 1021,
"preview": "import internalDeadHost from \"./dead-host.js\";\nimport internalProxyHost from \"./proxy-host.js\";\nimport internalRedirecti"
},
{
"path": "backend/internal/setting.js",
"chars": 2896,
"preview": "import fs from \"node:fs\";\nimport errs from \"../lib/error.js\";\nimport settingModel from \"../models/setting.js\";\nimport in"
},
{
"path": "backend/internal/stream.js",
"chars": 10458,
"preview": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { castJsonIfNeed } from \"../lib/helpers.js\";\nimport u"
},
{
"path": "backend/internal/token.js",
"chars": 5670,
"preview": "import _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport { parseDatePeriod } from \"../lib/helpers.js\";\nimport "
},
{
"path": "backend/internal/user.js",
"chars": 12337,
"preview": "import gravatar from \"gravatar\";\nimport _ from \"lodash\";\nimport errs from \"../lib/error.js\";\nimport utils from \"../lib/u"
},
{
"path": "backend/knexfile.js",
"chars": 341,
"preview": "module.exports = {\n\tdevelopment: {\n\t\tclient: 'mysql2',\n\t\tmigrations: {\n\t\t\ttableName: 'migrations',\n\t\t\tstub: 'li"
},
{
"path": "backend/lib/access/access_lists-create.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access"
},
{
"path": "backend/lib/access/access_lists-delete.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access"
},
{
"path": "backend/lib/access/access_lists-get.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access"
},
{
"path": "backend/lib/access/access_lists-list.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access"
},
{
"path": "backend/lib/access/access_lists-update.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_access"
},
{
"path": "backend/lib/access/auditlog-list.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/certificates-create.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certif"
},
{
"path": "backend/lib/access/certificates-delete.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certif"
},
{
"path": "backend/lib/access/certificates-get.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certif"
},
{
"path": "backend/lib/access/certificates-list.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certif"
},
{
"path": "backend/lib/access/certificates-update.json",
"chars": 366,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_certif"
},
{
"path": "backend/lib/access/dead_hosts-create.json",
"chars": 362,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_h"
},
{
"path": "backend/lib/access/dead_hosts-delete.json",
"chars": 362,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_h"
},
{
"path": "backend/lib/access/dead_hosts-get.json",
"chars": 360,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_h"
},
{
"path": "backend/lib/access/dead_hosts-list.json",
"chars": 360,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_h"
},
{
"path": "backend/lib/access/dead_hosts-update.json",
"chars": 362,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_dead_h"
},
{
"path": "backend/lib/access/permissions.json",
"chars": 178,
"preview": "{\n\t\"$id\": \"perms\",\n\t\"definitions\": {\n\t\t\"view\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"pattern\": \"^(view|manage)$\"\n\t\t},\n\t\t\"manage\": {"
},
{
"path": "backend/lib/access/proxy_hosts-create.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_"
},
{
"path": "backend/lib/access/proxy_hosts-delete.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_"
},
{
"path": "backend/lib/access/proxy_hosts-get.json",
"chars": 362,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_"
},
{
"path": "backend/lib/access/proxy_hosts-list.json",
"chars": 362,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_"
},
{
"path": "backend/lib/access/proxy_hosts-update.json",
"chars": 364,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_proxy_"
},
{
"path": "backend/lib/access/redirection_hosts-create.json",
"chars": 376,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redire"
},
{
"path": "backend/lib/access/redirection_hosts-delete.json",
"chars": 376,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redire"
},
{
"path": "backend/lib/access/redirection_hosts-get.json",
"chars": 374,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redire"
},
{
"path": "backend/lib/access/redirection_hosts-list.json",
"chars": 374,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redire"
},
{
"path": "backend/lib/access/redirection_hosts-update.json",
"chars": 376,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_redire"
},
{
"path": "backend/lib/access/reports-hosts.json",
"chars": 64,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/user\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/roles.json",
"chars": 586,
"preview": "{\n\t\"$id\": \"roles\",\n\t\"definitions\": {\n\t\t\"admin\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"scope\", \"roles\"],\n\t\t\t\"properties"
},
{
"path": "backend/lib/access/settings-get.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/settings-list.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/settings-update.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/streams-create.json",
"chars": 356,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_stream"
},
{
"path": "backend/lib/access/streams-delete.json",
"chars": 356,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_stream"
},
{
"path": "backend/lib/access/streams-get.json",
"chars": 354,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_stream"
},
{
"path": "backend/lib/access/streams-list.json",
"chars": 354,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_stream"
},
{
"path": "backend/lib/access/streams-update.json",
"chars": 356,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"permission_stream"
},
{
"path": "backend/lib/access/users-create.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/users-delete.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/users-get.json",
"chars": 334,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n"
},
{
"path": "backend/lib/access/users-list.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/users-loginas.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/users-password.json",
"chars": 334,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n"
},
{
"path": "backend/lib/access/users-permissions.json",
"chars": 65,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t}\n\t]\n}\n"
},
{
"path": "backend/lib/access/users-update.json",
"chars": 334,
"preview": "{\n\t\"anyOf\": [\n\t\t{\n\t\t\t\"$ref\": \"roles#/definitions/admin\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"object\",\n\t\t\t\"required\": [\"data\", \"scope\"],\n"
},
{
"path": "backend/lib/access.js",
"chars": 7302,
"preview": "/**\n * Some Notes: This is a friggin complicated piece of code.\n *\n * \"scope\" in this file means \"where did this token c"
},
{
"path": "backend/lib/certbot.js",
"chars": 2314,
"preview": "import batchflow from \"batchflow\";\nimport dnsPlugins from \"../certbot/dns-plugins.json\" with { type: \"json\" };\nimport { "
},
{
"path": "backend/lib/config.js",
"chars": 6725,
"preview": "import fs from \"node:fs\";\nimport NodeRSA from \"node-rsa\";\nimport { global as logger } from \"../logger.js\";\n\nconst keysFi"
},
{
"path": "backend/lib/error.js",
"chars": 2653,
"preview": "import _ from \"lodash\";\n\nconst errs = {\n\tPermissionError: function (_, previous) {\n\t\tError.captureStackTrace(this, this."
},
{
"path": "backend/lib/express/cors.js",
"chars": 570,
"preview": "export default (req, res, next) => {\n\tif (req.headers.origin) {\n\t\tres.set({\n\t\t\t\"Access-Control-Allow-Origin\": req.header"
},
{
"path": "backend/lib/express/jwt-decode.js",
"chars": 295,
"preview": "import Access from \"../access.js\";\n\nexport default () => {\n\treturn async (_, res, next) => {\n\t\ttry {\n\t\t\tres.locals.acces"
},
{
"path": "backend/lib/express/jwt.js",
"chars": 262,
"preview": "export default function () {\n\treturn (req, res, next) => {\n\t\tif (req.headers.authorization) {\n\t\t\tconst parts = req.heade"
},
{
"path": "backend/lib/express/pagination.js",
"chars": 1515,
"preview": "import _ from \"lodash\";\n\nexport default (default_sort, default_offset, default_limit, max_limit) => {\n\t/**\n\t * This wil"
},
{
"path": "backend/lib/express/user-id-from-me.js",
"chars": 247,
"preview": "export default (req, res, next) => {\n\tif (req.params.user_id === 'me' && res.locals.access) {\n\t\treq.params.user_id = res"
},
{
"path": "backend/lib/helpers.js",
"chars": 1347,
"preview": "import moment from \"moment\";\nimport { ref } from \"objection\";\nimport { isPostgres } from \"./config.js\";\n\n/**\n * Takes an"
},
{
"path": "backend/lib/migrate_template.js",
"chars": 1193,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"identifier_for_migrate\";\n\n/**\n * Migrate\n *\n * @"
},
{
"path": "backend/lib/utils.js",
"chars": 2529,
"preview": "import { exec as nodeExec, execFile as nodeExecFile } from \"node:child_process\";\nimport { dirname } from \"node:path\";\nim"
},
{
"path": "backend/lib/validator/api.js",
"chars": 928,
"preview": "import Ajv from \"ajv/dist/2020.js\";\nimport errs from \"../error.js\";\n\nconst ajv = new Ajv({\n\tverbose: true,\n\tallErrors: t"
},
{
"path": "backend/lib/validator/index.js",
"chars": 1018,
"preview": "import Ajv from 'ajv/dist/2020.js';\nimport _ from \"lodash\";\nimport commonDefinitions from \"../../schema/common.json\" wit"
},
{
"path": "backend/logger.js",
"chars": 1097,
"preview": "import signale from \"signale\";\nimport { isDebugMode } from \"./lib/config.js\";\n\nconst opts = {\n\tlogLevel: \"info\",\n};\n\ncon"
},
{
"path": "backend/migrate.js",
"chars": 335,
"preview": "import db from \"./db.js\";\nimport { migrate as logger } from \"./logger.js\";\n\nconst migrateUp = async () => {\n\tconst versi"
},
{
"path": "backend/migrations/20180618015850_initial.js",
"chars": 7554,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"initial-schema\";\n\n/**\n * Migrate\n *\n * @see http"
},
{
"path": "backend/migrations/20180929054513_websockets.js",
"chars": 730,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"websockets\";\n\n/**\n * Migrate\n *\n * @see http://k"
},
{
"path": "backend/migrations/20181019052346_forward_host.js",
"chars": 706,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"forward_host\";\n\n/**\n * Migrate\n *\n * @see http:/"
},
{
"path": "backend/migrations/20181113041458_http2_support.js",
"chars": 1193,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"http2_support\";\n\n/**\n * Migrate\n *\n * @see http:"
},
{
"path": "backend/migrations/20181213013211_forward_scheme.js",
"chars": 718,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"forward_scheme\";\n\n/**\n * Migrate\n *\n * @see http"
},
{
"path": "backend/migrations/20190104035154_disabled.js",
"chars": 1373,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"disabled\";\n\n/**\n * Migrate\n *\n * @see http://kne"
},
{
"path": "backend/migrations/20190215115310_customlocations.js",
"chars": 734,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"custom_locations\";\n\n/**\n * Migrate\n * Extends pr"
},
{
"path": "backend/migrations/20190218060101_hsts.js",
"chars": 1416,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"hsts\";\n\n/**\n * Migrate\n *\n * @see http://knexjs."
},
{
"path": "backend/migrations/20190227065017_settings.js",
"chars": 842,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"settings\";\n\n/**\n * Migrate\n *\n * @see http://kne"
},
{
"path": "backend/migrations/20200410143839_access_list_client.js",
"chars": 1263,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"access_list_client\";\n\n/**\n * Migrate\n *\n * @see "
},
{
"path": "backend/migrations/20200410143840_access_list_client_fix.js",
"chars": 719,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"access_list_client_fix\";\n\n/**\n * Migrate\n *\n * @"
},
{
"path": "backend/migrations/20201014143841_pass_auth.js",
"chars": 862,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"pass_auth\";\n\n/**\n * Migrate\n *\n * @see http://kn"
},
{
"path": "backend/migrations/20210210154702_redirection_scheme.js",
"chars": 874,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirection_scheme\";\n\n/**\n * Migrate\n *\n * @see "
},
{
"path": "backend/migrations/20210210154703_redirection_status_code.js",
"chars": 891,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirection_status_code\";\n\n/**\n * Migrate\n *\n * "
},
{
"path": "backend/migrations/20210423103500_stream_domain.js",
"chars": 836,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"stream_domain\";\n\n/**\n * Migrate\n *\n * @see http:"
},
{
"path": "backend/migrations/20211108145214_regenerate_default_host.js",
"chars": 989,
"preview": "import internalNginx from \"../internal/nginx.js\";\nimport { migrate as logger } from \"../logger.js\";\n\nconst migrateName ="
},
{
"path": "backend/migrations/20240427161436_stream_ssl.js",
"chars": 830,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"stream_ssl\";\n\n/**\n * Migrate\n *\n * @see http://k"
},
{
"path": "backend/migrations/20251111090000_redirect_auto_scheme.js",
"chars": 1223,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"redirect_auto_scheme\";\n\n/**\n * Migrate\n *\n * @se"
},
{
"path": "backend/migrations/20260131163528_trust_forwarded_proto.js",
"chars": 981,
"preview": "import { migrate as logger } from \"../logger.js\";\n\nconst migrateName = \"trust_forwarded_proto\";\n\n/**\n * Migrate\n *\n * @s"
},
{
"path": "backend/models/access_list.js",
"chars": 2112,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/access_list_auth.js",
"chars": 989,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/access_list_client.js",
"chars": 999,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/audit-log.js",
"chars": 834,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/auth.js",
"chars": 1796,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport bcrypt from \"bcrypt\";\nimport { Model } from \"objecti"
},
{
"path": "backend/models/certificate.js",
"chars": 2897,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/dead_host.js",
"chars": 2175,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/now_helper.js",
"chars": 254,
"preview": "import { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { isSqlite } from \"../lib/config.js\";\n\nModel.knex(d"
},
{
"path": "backend/models/proxy_host.js",
"chars": 2645,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/redirection_host.js",
"chars": 2263,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/setting.js",
"chars": 462,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/stream.js",
"chars": 1806,
"preview": "import { Model } from \"objection\";\nimport db from \"../db.js\";\nimport { castJsonIfNeed, convertBoolFieldsToInt, convertIn"
},
{
"path": "backend/models/token.js",
"chars": 3037,
"preview": "/**\n NOTE: This is not a database table, this is a model of a Token object that can be created/loaded\n and then has abil"
},
{
"path": "backend/models/user.js",
"chars": 1278,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/models/user_permission.js",
"chars": 492,
"preview": "// Objection Docs:\n// http://vincit.github.io/objection.js/\n\nimport { Model } from \"objection\";\nimport db from \"../db.js"
},
{
"path": "backend/nodemon.json",
"chars": 81,
"preview": "{\n \"verbose\": false,\n \"ignore\": [\n \"data\"\n ],\n \"ext\": \"js json ejs cjs\"\n}\n"
},
{
"path": "backend/package.json",
"chars": 1305,
"preview": "{\n\t\"name\": \"nginx-proxy-manager\",\n\t\"version\": \"2.0.0\",\n\t\"description\": \"A beautiful interface for creating Nginx endpoin"
},
{
"path": "backend/routes/audit-log.js",
"chars": 2206,
"preview": "import express from \"express\";\nimport internalAuditLog from \"../internal/audit-log.js\";\nimport jwtdecode from \"../lib/ex"
},
{
"path": "backend/routes/main.js",
"chars": 2104,
"preview": "import express from \"express\";\nimport errs from \"../lib/error.js\";\nimport pjson from \"../package.json\" with { type: \"jso"
},
{
"path": "backend/routes/nginx/access_lists.js",
"chars": 3742,
"preview": "import express from \"express\";\nimport internalAccessList from \"../../internal/access-list.js\";\nimport jwtdecode from \".."
},
{
"path": "backend/routes/nginx/certificates.js",
"chars": 7598,
"preview": "import express from \"express\";\nimport dnsPlugins from \"../../certbot/dns-plugins.json\" with { type: \"json\" };\nimport int"
},
{
"path": "backend/routes/nginx/dead_hosts.js",
"chars": 4745,
"preview": "import express from \"express\";\nimport internalDeadHost from \"../../internal/dead-host.js\";\nimport jwtdecode from \"../../"
},
{
"path": "backend/routes/nginx/proxy_hosts.js",
"chars": 4845,
"preview": "import express from \"express\";\nimport internalProxyHost from \"../../internal/proxy-host.js\";\nimport jwtdecode from \"../."
},
{
"path": "backend/routes/nginx/redirection_hosts.js",
"chars": 5001,
"preview": "import express from \"express\";\nimport internalRedirectionHost from \"../../internal/redirection-host.js\";\nimport jwtdecod"
},
{
"path": "backend/routes/nginx/streams.js",
"chars": 4823,
"preview": "import express from \"express\";\nimport internalStream from \"../../internal/stream.js\";\nimport jwtdecode from \"../../lib/e"
},
{
"path": "backend/routes/reports.js",
"chars": 697,
"preview": "import express from \"express\";\nimport internalReport from \"../internal/report.js\";\nimport jwtdecode from \"../lib/express"
},
{
"path": "backend/routes/schema.js",
"chars": 1095,
"preview": "import express from \"express\";\nimport { debug, express as logger } from \"../logger.js\";\nimport PACKAGE from \"../package."
},
{
"path": "backend/routes/settings.js",
"chars": 2163,
"preview": "import express from \"express\";\nimport internalSetting from \"../internal/setting.js\";\nimport jwtdecode from \"../lib/expre"
},
{
"path": "backend/routes/tokens.js",
"chars": 2050,
"preview": "import express from \"express\";\nimport internalToken from \"../internal/token.js\";\nimport jwtdecode from \"../lib/express/j"
},
{
"path": "backend/routes/users.js",
"chars": 9978,
"preview": "import express from \"express\";\nimport internal2FA from \"../internal/2fa.js\";\nimport internalUser from \"../internal/user."
},
{
"path": "backend/routes/version.js",
"chars": 867,
"preview": "import express from \"express\";\nimport internalRemoteVersion from \"../internal/remote-version.js\";\nimport { debug, expres"
},
{
"path": "backend/schema/common.json",
"chars": 5979,
"preview": "{\n\t\"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n\t\"$id\": \"common\",\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"i"
},
{
"path": "backend/schema/components/access-list-object.json",
"chars": 849,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Access List object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"owner_user_"
},
{
"path": "backend/schema/components/audit-log-list.json",
"chars": 107,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Audit Log list\",\n\t\"items\": {\n\t\t\"$ref\": \"./audit-log-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/audit-log-object.json",
"chars": 818,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Audit Log object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"user"
},
{
"path": "backend/schema/components/certificate-list.json",
"chars": 112,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Certificates list\",\n\t\"items\": {\n\t\t\"$ref\": \"./certificate-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/certificate-object.json",
"chars": 1922,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Certificate object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"owner_user_"
},
{
"path": "backend/schema/components/check-version-object.json",
"chars": 533,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Check Version object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"current\", \"l"
},
{
"path": "backend/schema/components/dead-host-list.json",
"chars": 107,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"404 Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./dead-host-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/dead-host-object.json",
"chars": 1482,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"404 Host object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"owner_user_id\""
},
{
"path": "backend/schema/components/dns-providers-list.json",
"chars": 561,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"DNS Providers list\",\n\t\"items\": {\n\t\t\"type\": \"object\",\n\t\t\"required\": [\"id\", \"name\", \""
},
{
"path": "backend/schema/components/error-object.json",
"chars": 266,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Error object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"code\", \"message\"],\n\t"
},
{
"path": "backend/schema/components/error.json",
"chars": 118,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Error\",\n\t\"properties\": {\n\t\t\"error\": {\n\t\t\t\"$ref\": \"./error-object.json\"\n\t\t}\n\t}\n}\n"
},
{
"path": "backend/schema/components/health-object.json",
"chars": 866,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Health object\",\n\t\"additionalProperties\": false,\n\t\"required\": [\"status\", \"version\"]"
},
{
"path": "backend/schema/components/permission-object.json",
"chars": 1111,
"preview": "{\n\t\"type\": \"object\",\n\t\"minProperties\": 1,\n\t\"properties\": {\n\t\t\"visibility\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"description\": \"Vis"
},
{
"path": "backend/schema/components/proxy-host-list.json",
"chars": 110,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Proxy Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./proxy-host-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/proxy-host-object.json",
"chars": 3555,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Proxy Host object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"own"
},
{
"path": "backend/schema/components/redirection-host-list.json",
"chars": 122,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Redirection Hosts list\",\n\t\"items\": {\n\t\t\"$ref\": \"./redirection-host-object.json\"\n\t}\n"
},
{
"path": "backend/schema/components/redirection-host-object.json",
"chars": 2306,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Redirection Host object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n"
},
{
"path": "backend/schema/components/security-schemes.json",
"chars": 140,
"preview": "{\n\t\"bearerAuth\": {\n\t\t\"type\": \"http\",\n\t\t\"scheme\": \"bearer\",\n\t\t\"bearerFormat\": \"JWT\",\n\t\t\"description\": \"JWT Bearer Token a"
},
{
"path": "backend/schema/components/setting-list.json",
"chars": 103,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Setting list\",\n\t\"items\": {\n\t\t\"$ref\": \"./setting-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/setting-object.json",
"chars": 1060,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Setting object\",\n\t\"required\": [\"id\", \"name\", \"description\", \"value\", \"meta\"],\n\t\"ad"
},
{
"path": "backend/schema/components/stream-list.json",
"chars": 102,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"Streams list\",\n\t\"items\": {\n\t\t\"$ref\": \"./stream-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/stream-object.json",
"chars": 1722,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Stream object\",\n\t\"required\": [\n\t\t\"id\",\n\t\t\"created_on\",\n\t\t\"modified_on\",\n\t\t\"owner_u"
},
{
"path": "backend/schema/components/token-challenge.json",
"chars": 504,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Token object\",\n\t\"required\": [\"requires_2fa\", \"challenge_token\"],\n\t\"additionalPrope"
},
{
"path": "backend/schema/components/token-object.json",
"chars": 420,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"Token object\",\n\t\"required\": [\"expires\", \"token\"],\n\t\"additionalProperties\": false,\n"
},
{
"path": "backend/schema/components/user-list.json",
"chars": 97,
"preview": "{\n\t\"type\": \"array\",\n\t\"description\": \"User list\",\n\t\"items\": {\n\t\t\"$ref\": \"./user-object.json\"\n\t}\n}\n"
},
{
"path": "backend/schema/components/user-object.json",
"chars": 2733,
"preview": "{\n\t\"type\": \"object\",\n\t\"description\": \"User object\",\n\t\"required\": [\"id\", \"created_on\", \"modified_on\", \"is_disabled\", \"ema"
},
{
"path": "backend/schema/index.js",
"chars": 1498,
"preview": "import { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport $RefParser from \"@apidevtools/json"
},
{
"path": "backend/schema/paths/audit-log/get.json",
"chars": 1173,
"preview": "{\n\t\"operationId\": \"getAuditLogs\",\n\t\"summary\": \"Get Audit Logs\",\n\t\"tags\": [\"audit-log\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAut"
},
{
"path": "backend/schema/paths/audit-log/id/get.json",
"chars": 1557,
"preview": "{\n\t\"operationId\": \"getAuditLog\",\n\t\"summary\": \"Get Audit Log Event\",\n\t\"tags\": [\"audit-log\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"beare"
},
{
"path": "backend/schema/paths/get.json",
"chars": 514,
"preview": "{\n\t\"operationId\": \"health\",\n\t\"summary\": \"Returns the API health status\",\n\t\"tags\": [\"public\"],\n\t\"responses\": {\n\t\t\"200\": {"
},
{
"path": "backend/schema/paths/nginx/access-lists/get.json",
"chars": 895,
"preview": "{\n\t\"operationId\": \"getAccessLists\",\n\t\"summary\": \"Get all access lists\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n\t\t"
},
{
"path": "backend/schema/paths/nginx/access-lists/listID/delete.json",
"chars": 616,
"preview": "{\n\t\"operationId\": \"deleteAccessList\",\n\t\"summary\": \"Delete a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n"
},
{
"path": "backend/schema/paths/nginx/access-lists/listID/get.json",
"chars": 1567,
"preview": "{\n \"operationId\": \"getAccessList\",\n \"summary\": \"Get a access List\",\n \"tags\": [\n \"access-lists\"\n ],\n "
},
{
"path": "backend/schema/paths/nginx/access-lists/listID/put.json",
"chars": 3371,
"preview": "{\n\t\"operationId\": \"updateAccessList\",\n\t\"summary\": \"Update a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n"
},
{
"path": "backend/schema/paths/nginx/access-lists/post.json",
"chars": 3194,
"preview": "{\n\t\"operationId\": \"createAccessList\",\n\t\"summary\": \"Create a Access List\",\n\t\"tags\": [\"access-lists\"],\n\t\"security\": [\n\t\t{\n"
},
{
"path": "backend/schema/paths/nginx/certificates/certID/delete.json",
"chars": 617,
"preview": "{\n\t\"operationId\": \"deleteCertificate\",\n\t\"summary\": \"Delete a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{"
},
{
"path": "backend/schema/paths/nginx/certificates/certID/download/get.json",
"chars": 571,
"preview": "{\n\t\"operationId\": \"downloadCertificate\",\n\t\"summary\": \"Downloads a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": "
},
{
"path": "backend/schema/paths/nginx/certificates/certID/get.json",
"chars": 1032,
"preview": "{\n\t\"operationId\": \"getCertificate\",\n\t\"summary\": \"Get a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"b"
},
{
"path": "backend/schema/paths/nginx/certificates/certID/renew/post.json",
"chars": 1045,
"preview": "{\n\t\"operationId\": \"renewCertificate\",\n\t\"summary\": \"Renews a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n"
},
{
"path": "backend/schema/paths/nginx/certificates/certID/upload/post.json",
"chars": 4753,
"preview": "{\n\t\"operationId\": \"uploadCertificate\",\n\t\"summary\": \"Uploads a custom Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security"
},
{
"path": "backend/schema/paths/nginx/certificates/dns-providers/get.json",
"chars": 1145,
"preview": "{\n\t\"operationId\": \"getDNSProviders\",\n\t\"summary\": \"Get DNS Providers for Certificates\",\n\t\"tags\": [\"certificates\"],\n\t\"secu"
},
{
"path": "backend/schema/paths/nginx/certificates/get.json",
"chars": 1025,
"preview": "{\n\t\"operationId\": \"getCertificates\",\n\t\"summary\": \"Get all certificates\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t"
},
{
"path": "backend/schema/paths/nginx/certificates/post.json",
"chars": 2271,
"preview": "{\n\t\"operationId\": \"createCertificate\",\n\t\"summary\": \"Create a Certificate\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{"
},
{
"path": "backend/schema/paths/nginx/certificates/test-http/post.json",
"chars": 858,
"preview": "{\n\t\"operationId\": \"testHttpReach\",\n\t\"summary\": \"Test HTTP Reachability\",\n\t\"tags\": [\"certificates\"],\n\t\"security\": [\n\t\t{\n\t"
},
{
"path": "backend/schema/paths/nginx/certificates/validate/post.json",
"chars": 2190,
"preview": "{\n\t\"operationId\": \"validateCertificates\",\n\t\"summary\": \"Validates given Custom Certificates\",\n\t\"tags\": [\"certificates\"],\n"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/get.json",
"chars": 1143,
"preview": "{\n\t\"operationId\": \"getDeadHosts\",\n\t\"summary\": \"Get all 404 hosts\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearer"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/hostID/delete.json",
"chars": 614,
"preview": "{\n\t\"operationId\": \"deleteDeadHost\",\n\t\"summary\": \"Delete a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bear"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json",
"chars": 975,
"preview": "{\n\t\"operationId\": \"disableDeadHost\",\n\t\"summary\": \"Disable a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"be"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json",
"chars": 972,
"preview": "{\n\t\"operationId\": \"enableDeadHost\",\n\t\"summary\": \"Enable a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bear"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/hostID/get.json",
"chars": 1138,
"preview": "{\n\t\"operationId\": \"getDeadHost\",\n\t\"summary\": \"Get a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bearerAuth"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/hostID/put.json",
"chars": 2730,
"preview": "{\n\t\"operationId\": \"updateDeadHost\",\n\t\"summary\": \"Update a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"bear"
},
{
"path": "backend/schema/paths/nginx/dead-hosts/post.json",
"chars": 2762,
"preview": "{\n\t\"operationId\": \"create404Host\",\n\t\"summary\": \"Create a 404 Host\",\n\t\"tags\": [\"404-hosts\"],\n\t\"security\": [\n\t\t{\n\t\t\t\"beare"
}
]
// ... and 589 more files (download for full content)
About this extraction
This page contains the full source code of the NginxProxyManager/nginx-proxy-manager GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 789 files (1.5 MB), approximately 464.2k tokens, and a symbol index with 437 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.