From 2110ecc3826f33ea738f74f280ec785411ee5984 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 12 May 2022 08:47:31 +1000 Subject: [PATCH] Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager --- .dockerignore | 8 + .gitignore | 24 +- .version | 2 +- DEV-README.md | 93 + Jenkinsfile | 167 +- README.md | 442 +- backend/.editorconfig | 8 + backend/.eslintrc.json | 73 - backend/.gitignore | 8 - backend/.golangci.yml | 92 + backend/.nancy-ignore | 22 + backend/.vscode/settings.json | 8 - backend/README.md | 6 + backend/Taskfile.yml | 69 + backend/app.js | 89 - backend/cmd/server/main.go | 47 + backend/config/README.md | 2 - backend/config/default.json | 10 - backend/config/sqlite-test-db.json | 26 - backend/db.js | 33 - backend/doc/api.swagger.json | 1254 -- backend/embed/api_docs/api.swagger.json | 243 + .../components/CertificateAuthorityList.json | 40 + .../CertificateAuthorityObject.json | 55 + .../api_docs/components/CertificateList.json | 40 + .../components/CertificateObject.json | 82 + .../api_docs/components/ConfigObject.json | 4 + .../api_docs/components/DNSProviderList.json | 40 + .../components/DNSProviderObject.json | 49 + .../components/DeletedItemResponse.json | 15 + .../api_docs/components/ErrorObject.json | 17 + .../api_docs/components/FilterObject.json | 24 + .../api_docs/components/HealthObject.json | 41 + .../embed/api_docs/components/HostList.json | 40 + .../embed/api_docs/components/HostObject.json | 55 + .../api_docs/components/HostTemplateList.json | 40 + .../components/HostTemplateObject.json | 44 + .../api_docs/components/SettingList.json | 40 + .../api_docs/components/SettingObject.json | 49 + .../embed/api_docs/components/SortObject.json | 17 + .../embed/api_docs/components/StreamList.json | 40 + .../api_docs/components/StreamObject.json | 55 + .../api_docs/components/TokenObject.json | 19 + .../api_docs/components/UserAuthObject.json | 28 + .../embed/api_docs/components/UserList.json | 40 + .../embed/api_docs/components/UserObject.json | 73 + backend/embed/api_docs/main.go | 9 + .../certificates-authorities/caID/delete.json | 39 + .../certificates-authorities/caID/get.json | 52 + .../certificates-authorities/caID/put.json | 61 + .../paths/certificates-authorities/get.json | 92 + .../paths/certificates-authorities/post.json | 48 + .../certificates/certificateID/delete.json | 60 + .../paths/certificates/certificateID/get.json | 61 + .../paths/certificates/certificateID/put.json | 70 + .../api_docs/paths/certificates/get.json | 90 + .../api_docs/paths/certificates/post.json | 57 + backend/embed/api_docs/paths/config/get.json | 36 + .../api_docs/paths/dns-providers/get.json | 82 + .../api_docs/paths/dns-providers/post.json | 49 + .../dns-providers/providerID/delete.json | 60 + .../paths/dns-providers/providerID/get.json | 53 + .../paths/dns-providers/providerID/put.json | 64 + backend/embed/api_docs/paths/get.json | 47 + .../api_docs/paths/host-templates/get.json | 79 + .../host-templates/hostTemplateID/delete.json | 58 + .../host-templates/hostTemplateID/get.json | 50 + .../host-templates/hostTemplateID/put.json | 59 + .../api_docs/paths/host-templates/post.json | 46 + backend/embed/api_docs/paths/hosts/get.json | 94 + .../api_docs/paths/hosts/hostID/delete.json | 60 + .../api_docs/paths/hosts/hostID/get.json | 65 + .../api_docs/paths/hosts/hostID/put.json | 74 + backend/embed/api_docs/paths/hosts/post.json | 61 + backend/embed/api_docs/paths/schema/get.json | 9 + .../embed/api_docs/paths/settings/get.json | 84 + .../api_docs/paths/settings/name/get.json | 55 + .../api_docs/paths/settings/name/put.json | 64 + .../embed/api_docs/paths/settings/post.json | 51 + backend/embed/api_docs/paths/streams/get.json | 75 + .../embed/api_docs/paths/streams/post.json | 42 + .../paths/streams/streamID/delete.json | 60 + .../api_docs/paths/streams/streamID/get.json | 46 + .../api_docs/paths/streams/streamID/put.json | 55 + backend/embed/api_docs/paths/tokens/get.json | 37 + backend/embed/api_docs/paths/tokens/post.json | 79 + backend/embed/api_docs/paths/users/get.json | 117 + backend/embed/api_docs/paths/users/post.json | 79 + .../paths/users/userID/auth/post.json | 65 + .../api_docs/paths/users/userID/delete.json | 60 + .../api_docs/paths/users/userID/get.json | 60 + .../api_docs/paths/users/userID/put.json | 107 + backend/embed/main.go | 19 + .../20201013035318_initial_schema.sql | 209 + .../20201013035839_initial_data.sql | 171 + backend/embed/nginx/_assets.conf.hbs | 4 + backend/embed/nginx/_certificates.conf.hbs | 13 + backend/embed/nginx/_forced_ssl.conf.hbs | 6 + backend/embed/nginx/_hsts.conf.hbs | 8 + backend/embed/nginx/_listen.conf.hbs | 18 + backend/embed/nginx/_location.conf.hbs | 40 + backend/embed/nginx/acme-request.conf.hbs | 15 + backend/embed/nginx/dead_host.conf.hbs | 20 + backend/embed/nginx/default.conf.hbs | 35 + backend/embed/nginx/ip_ranges.conf.hbs | 3 + backend/embed/nginx/proxy_host.conf.hbs | 62 + backend/embed/nginx/redirection_host.conf.hbs | 28 + backend/embed/nginx/stream.conf.hbs | 34 + backend/go.mod | 39 + backend/go.sum | 300 + backend/index.js | 135 - backend/internal/access-list.js | 534 - backend/internal/acme/acmesh.go | 200 + backend/internal/acme/acmesh_test.go | 204 + backend/internal/acme/errors.go | 10 + backend/internal/api/context/context.go | 25 + backend/internal/api/filters/helpers.go | 208 + backend/internal/api/handler/auth.go | 93 + .../api/handler/certificate_authorities.go | 141 + backend/internal/api/handler/certificates.go | 145 + backend/internal/api/handler/config.go | 15 + backend/internal/api/handler/dns_providers.go | 159 + backend/internal/api/handler/health.go | 34 + backend/internal/api/handler/helpers.go | 175 + .../internal/api/handler/host_templates.go | 130 + backend/internal/api/handler/hosts.go | 140 + backend/internal/api/handler/not_allowed.go | 14 + backend/internal/api/handler/not_found.go | 64 + backend/internal/api/handler/schema.go | 108 + backend/internal/api/handler/settings.go | 98 + backend/internal/api/handler/streams.go | 129 + backend/internal/api/handler/tokens.go | 89 + backend/internal/api/handler/users.go | 235 + backend/internal/api/http/requests.go | 46 + backend/internal/api/http/responses.go | 91 + .../internal/api/middleware/access_control.go | 13 + backend/internal/api/middleware/auth.go | 94 + backend/internal/api/middleware/auth_cache.go | 23 + .../internal/api/middleware/body_context.go | 26 + backend/internal/api/middleware/cors.go | 88 + .../internal/api/middleware/enforce_setup.go | 28 + backend/internal/api/middleware/expansion.go | 24 + backend/internal/api/middleware/filters.go | 115 + .../internal/api/middleware/pretty_print.go | 23 + backend/internal/api/middleware/schema.go | 55 + backend/internal/api/router.go | 198 + backend/internal/api/router_test.go | 44 + backend/internal/api/schema/certificates.go | 209 + backend/internal/api/schema/common.go | 70 + .../schema/create_certificate_authority.go | 25 + .../api/schema/create_dns_provider.go | 51 + backend/internal/api/schema/create_host.go | 80 + .../api/schema/create_host_template.go | 30 + backend/internal/api/schema/create_setting.go | 21 + backend/internal/api/schema/create_stream.go | 27 + backend/internal/api/schema/create_user.go | 42 + backend/internal/api/schema/get_token.go | 28 + backend/internal/api/schema/set_auth.go | 25 + .../schema/update_certificate_authority.go | 21 + .../api/schema/update_dns_provider.go | 20 + backend/internal/api/schema/update_host.go | 27 + .../api/schema/update_host_template.go | 22 + backend/internal/api/schema/update_setting.go | 17 + backend/internal/api/schema/update_stream.go | 23 + backend/internal/api/schema/update_user.go | 23 + backend/internal/api/server.go | 19 + backend/internal/audit-log.js | 78 - backend/internal/cache/cache.go | 51 + backend/internal/certificate.js | 1223 -- backend/internal/config/args.go | 28 + backend/internal/config/config.go | 79 + backend/internal/config/folders.go | 34 + backend/internal/config/keys.go | 112 + backend/internal/config/vars.go | 49 + backend/internal/database/helpers.go | 46 + backend/internal/database/migrator.go | 202 + backend/internal/database/setup.go | 37 + backend/internal/database/sqlite.go | 74 + backend/internal/dead-host.js | 461 - backend/internal/dnsproviders/common.go | 135 + backend/internal/dnsproviders/dns_ad.go | 17 + backend/internal/dnsproviders/dns_ali.go | 25 + backend/internal/dnsproviders/dns_aws.go | 56 + backend/internal/dnsproviders/dns_cf.go | 80 + backend/internal/dnsproviders/dns_cloudns.go | 54 + backend/internal/dnsproviders/dns_cx.go | 25 + backend/internal/dnsproviders/dns_cyon.go | 57 + backend/internal/dnsproviders/dns_dgon.go | 18 + backend/internal/dnsproviders/dns_dnsimple.go | 18 + backend/internal/dnsproviders/dns_dp.go | 46 + backend/internal/dnsproviders/dns_duckdns.go | 18 + backend/internal/dnsproviders/dns_dyn.go | 58 + backend/internal/dnsproviders/dns_dynu.go | 25 + backend/internal/dnsproviders/dns_freedns.go | 46 + .../dnsproviders/dns_gandi_livedns.go | 17 + backend/internal/dnsproviders/dns_gd.go | 25 + backend/internal/dnsproviders/dns_he.go | 47 + backend/internal/dnsproviders/dns_infoblox.go | 46 + .../internal/dnsproviders/dns_ispconfig.go | 67 + .../internal/dnsproviders/dns_linode_v4.go | 20 + backend/internal/dnsproviders/dns_lua.go | 46 + backend/internal/dnsproviders/dns_me.go | 25 + backend/internal/dnsproviders/dns_namecom.go | 46 + backend/internal/dnsproviders/dns_nsone.go | 18 + backend/internal/dnsproviders/dns_pdns.go | 69 + backend/internal/dnsproviders/dns_unoeuro.go | 47 + backend/internal/dnsproviders/dns_vscale.go | 17 + backend/internal/dnsproviders/dns_yandex.go | 18 + backend/internal/entity/auth/methods.go | 82 + backend/internal/entity/auth/model.go | 98 + .../internal/entity/certificate/filters.go | 25 + .../internal/entity/certificate/methods.go | 174 + backend/internal/entity/certificate/model.go | 266 + .../internal/entity/certificate/structs.go | 15 + .../entity/certificateauthority/filters.go | 25 + .../entity/certificateauthority/methods.go | 134 + .../entity/certificateauthority/model.go | 88 + .../entity/certificateauthority/structs.go | 15 + .../internal/entity/dnsprovider/filters.go | 25 + .../internal/entity/dnsprovider/methods.go | 134 + backend/internal/entity/dnsprovider/model.go | 101 + .../internal/entity/dnsprovider/model_test.go | 82 + .../internal/entity/dnsprovider/structs.go | 15 + backend/internal/entity/filters.go | 158 + backend/internal/entity/filters_schema.go | 223 + backend/internal/entity/host/filters.go | 25 + backend/internal/entity/host/methods.go | 183 + backend/internal/entity/host/model.go | 120 + backend/internal/entity/host/structs.go | 15 + .../internal/entity/hosttemplate/filters.go | 25 + .../internal/entity/hosttemplate/methods.go | 129 + backend/internal/entity/hosttemplate/model.go | 73 + .../internal/entity/hosttemplate/structs.go | 15 + backend/internal/entity/lists_query.go | 80 + backend/internal/entity/setting/apply.go | 19 + backend/internal/entity/setting/filters.go | 25 + backend/internal/entity/setting/methods.go | 127 + backend/internal/entity/setting/model.go | 70 + backend/internal/entity/setting/structs.go | 15 + backend/internal/entity/stream/filters.go | 25 + backend/internal/entity/stream/methods.go | 135 + backend/internal/entity/stream/model.go | 75 + backend/internal/entity/stream/structs.go | 15 + backend/internal/entity/user/capabilities.go | 40 + backend/internal/entity/user/filters.go | 25 + backend/internal/entity/user/methods.go | 229 + backend/internal/entity/user/model.go | 191 + backend/internal/entity/user/structs.go | 15 + backend/internal/errors/errors.go | 16 + backend/internal/host.js | 235 - backend/internal/ip_ranges.js | 150 - backend/internal/jwt/jwt.go | 60 + backend/internal/jwt/keys.go | 86 + backend/internal/logger/config.go | 40 + backend/internal/logger/logger.go | 242 + backend/internal/logger/logger_test.go | 168 + backend/internal/model/filter.go | 8 + backend/internal/model/pageinfo.go | 22 + backend/internal/nginx.js | 435 - backend/internal/nginx/templates.go | 31 + backend/internal/proxy-host.js | 466 - backend/internal/redirection-host.js | 461 - backend/internal/report.js | 38 - backend/internal/setting.js | 133 - backend/internal/state/state.go | 31 + backend/internal/stream.js | 348 - backend/internal/token.js | 162 - backend/internal/types/db_date.go | 39 + backend/internal/types/jsonb.go | 71 + backend/internal/types/nullable_db_date.go | 54 + backend/internal/user.js | 518 - backend/internal/util/interfaces.go | 36 + backend/internal/util/maps.go | 9 + backend/internal/util/maps_test.go | 45 + backend/internal/util/slices.go | 44 + backend/internal/util/slices_test.go | 92 + backend/internal/validator/hosts.go | 33 + backend/internal/worker/certificate.go | 63 + backend/knexfile.js | 19 - backend/lib/access.js | 314 - backend/lib/access/access_lists-create.json | 23 - backend/lib/access/access_lists-delete.json | 23 - backend/lib/access/access_lists-get.json | 23 - backend/lib/access/access_lists-list.json | 23 - backend/lib/access/access_lists-update.json | 23 - backend/lib/access/auditlog-list.json | 7 - backend/lib/access/certificates-create.json | 23 - backend/lib/access/certificates-delete.json | 23 - backend/lib/access/certificates-get.json | 23 - backend/lib/access/certificates-list.json | 23 - backend/lib/access/certificates-update.json | 23 - backend/lib/access/dead_hosts-create.json | 23 - backend/lib/access/dead_hosts-delete.json | 23 - backend/lib/access/dead_hosts-get.json | 23 - backend/lib/access/dead_hosts-list.json | 23 - backend/lib/access/dead_hosts-update.json | 23 - backend/lib/access/permissions.json | 14 - backend/lib/access/proxy_hosts-create.json | 23 - backend/lib/access/proxy_hosts-delete.json | 23 - backend/lib/access/proxy_hosts-get.json | 23 - backend/lib/access/proxy_hosts-list.json | 23 - backend/lib/access/proxy_hosts-update.json | 23 - .../lib/access/redirection_hosts-create.json | 23 - .../lib/access/redirection_hosts-delete.json | 23 - backend/lib/access/redirection_hosts-get.json | 23 - .../lib/access/redirection_hosts-list.json | 23 - .../lib/access/redirection_hosts-update.json | 23 - backend/lib/access/reports-hosts.json | 7 - backend/lib/access/roles.json | 39 - backend/lib/access/settings-get.json | 7 - backend/lib/access/settings-list.json | 7 - backend/lib/access/settings-update.json | 7 - backend/lib/access/streams-create.json | 23 - backend/lib/access/streams-delete.json | 23 - backend/lib/access/streams-get.json | 23 - backend/lib/access/streams-list.json | 23 - backend/lib/access/streams-update.json | 23 - backend/lib/access/users-create.json | 7 - backend/lib/access/users-delete.json | 7 - backend/lib/access/users-get.json | 23 - backend/lib/access/users-list.json | 7 - backend/lib/access/users-loginas.json | 7 - backend/lib/access/users-password.json | 23 - backend/lib/access/users-permissions.json | 7 - backend/lib/access/users-update.json | 23 - backend/lib/error.js | 90 - backend/lib/express/cors.js | 40 - backend/lib/express/jwt-decode.js | 15 - backend/lib/express/jwt.js | 13 - backend/lib/express/pagination.js | 55 - backend/lib/express/user-id-from-me.js | 9 - backend/lib/helpers.js | 32 - backend/lib/migrate_template.js | 55 - backend/lib/utils.js | 20 - backend/lib/validator/api.js | 45 - backend/lib/validator/index.js | 49 - backend/logger.js | 13 - backend/migrate.js | 15 - backend/migrations/20180618015850_initial.js | 205 - .../migrations/20180929054513_websockets.js | 35 - .../migrations/20181019052346_forward_host.js | 34 - .../20181113041458_http2_support.js | 49 - .../20181213013211_forward_scheme.js | 34 - backend/migrations/20190104035154_disabled.js | 55 - .../20190215115310_customlocations.js | 35 - backend/migrations/20190218060101_hsts.js | 51 - backend/migrations/20190227065017_settings.js | 38 - .../20200410143839_access_list_client.js | 53 - .../20200410143840_access_list_client_fix.js | 34 - .../migrations/20201014143841_pass_auth.js | 41 - .../20210210154702_redirection_scheme.js | 41 - .../20210210154703_redirection_status_code.js | 41 - .../20210423103500_stream_domain.js | 40 - .../20211108145214_regenerate_default_host.js | 50 - backend/models/access_list.js | 102 - backend/models/access_list_auth.js | 55 - backend/models/access_list_client.js | 59 - backend/models/audit-log.js | 55 - backend/models/auth.js | 86 - backend/models/certificate.js | 73 - backend/models/dead_host.js | 81 - backend/models/now_helper.js | 13 - backend/models/proxy_host.js | 94 - backend/models/redirection_host.js | 81 - backend/models/setting.js | 30 - backend/models/stream.js | 56 - backend/models/token.js | 147 - backend/models/user.js | 56 - backend/models/user_permission.js | 29 - backend/nodemon.json | 7 - backend/package.json | 43 - backend/routes/api/audit-log.js | 52 - backend/routes/api/main.js | 51 - backend/routes/api/nginx/access_lists.js | 148 - backend/routes/api/nginx/certificates.js | 299 - backend/routes/api/nginx/dead_hosts.js | 196 - backend/routes/api/nginx/proxy_hosts.js | 196 - backend/routes/api/nginx/redirection_hosts.js | 196 - backend/routes/api/nginx/streams.js | 196 - backend/routes/api/reports.js | 29 - backend/routes/api/schema.js | 36 - backend/routes/api/settings.js | 96 - backend/routes/api/tokens.js | 54 - backend/routes/api/users.js | 239 - backend/schema/definitions.json | 240 - backend/schema/endpoints/access-lists.json | 236 - backend/schema/endpoints/certificates.json | 173 - backend/schema/endpoints/dead-hosts.json | 240 - backend/schema/endpoints/proxy-hosts.json | 387 - .../schema/endpoints/redirection-hosts.json | 305 - backend/schema/endpoints/settings.json | 99 - backend/schema/endpoints/streams.json | 234 - backend/schema/endpoints/tokens.json | 100 - backend/schema/endpoints/users.json | 287 - backend/schema/examples.json | 23 - backend/schema/index.json | 42 - backend/scripts/lint.sh | 21 + backend/scripts/test.sh | 5 + backend/setup.js | 233 - backend/templates/_assets.conf | 4 - backend/templates/_certificates.conf | 14 - backend/templates/_exploits.conf | 4 - backend/templates/_forced_ssl.conf | 6 - backend/templates/_header_comment.conf | 3 - backend/templates/_hsts.conf | 8 - backend/templates/_listen.conf | 15 - backend/templates/_location.conf | 45 - backend/templates/dead_host.conf | 23 - backend/templates/default.conf | 40 - backend/templates/ip_ranges.conf | 3 - backend/templates/letsencrypt-request.conf | 19 - backend/templates/proxy_host.conf | 70 - backend/templates/redirection_host.conf | 32 - backend/templates/stream.conf | 37 - backend/yarn.lock | 3754 ----- docker/Dockerfile | 144 +- docker/dev/Dockerfile | 56 +- docker/dev/dnsrouter-config.json | 28 + docker/dev/pdns-db.sql | 87 + docker/dev/pebble-config.json | 12 + docker/docker-compose.ci.yml | 129 +- docker/docker-compose.dev.yml | 141 +- docker/rootfs/bin/check-health | 11 - docker/rootfs/bin/handle-ipv6-setting | 46 - docker/rootfs/bin/healthcheck.sh | 7 + docker/rootfs/etc/cont-init.d/.gitignore | 3 - docker/rootfs/etc/cont-init.d/01_perms.sh | 7 - .../etc/cont-init.d/01_s6-secret-init.sh | 29 - docker/rootfs/etc/cont-init.d/10-nginx | 18 + docker/rootfs/etc/cont-init.d/20-adduser | 33 + docker/rootfs/etc/letsencrypt.ini | 6 - .../etc/logrotate.d/nginx-proxy-manager | 25 - docker/rootfs/etc/nginx/conf.d/default.conf | 33 +- docker/rootfs/etc/nginx/conf.d/dev.conf | 22 +- .../etc/nginx/conf.d/include/.gitignore | 1 - .../nginx/conf.d/include/acme-challenge.conf | 17 + .../etc/nginx/conf.d/include/assets.conf | 42 +- .../nginx/conf.d/include/block-exploits.conf | 60 +- .../etc/nginx/conf.d/include/force-ssl.conf | 2 +- .../etc/nginx/conf.d/include/ip_ranges.conf | 2 - .../include/letsencrypt-acme-challenge.conf | 30 - .../etc/nginx/conf.d/include/proxy.conf | 4 +- .../etc/nginx/conf.d/include/resolvers.conf | 1 + .../etc/nginx/conf.d/include/ssl-ciphers.conf | 6 +- .../rootfs/etc/nginx/conf.d/production.conf | 23 +- docker/rootfs/etc/nginx/nginx.conf | 53 +- docker/rootfs/etc/services.d/backend/finish | 5 + docker/rootfs/etc/services.d/backend/run | 23 + docker/rootfs/etc/services.d/frontend/finish | 1 - docker/rootfs/etc/services.d/frontend/run | 9 +- docker/rootfs/etc/services.d/manager/finish | 3 - docker/rootfs/etc/services.d/manager/run | 19 - docker/rootfs/etc/services.d/nginx/finish | 7 +- docker/rootfs/etc/services.d/nginx/run | 48 +- docker/rootfs/root/.bashrc | 6 +- docker/rootfs/root/.config/litecli/config | 13 + docker/rootfs/var/www/html/index.html | 42 +- docs/.gitignore | 2 + docs/.vuepress/config.js | 2 +- docs/README.md | 36 +- docs/advanced-config/README.md | 28 +- docs/faq/README.md | 3 - docs/package.json | 10 +- docs/setup/README.md | 156 +- docs/yarn.lock | 139 +- frontend/.babelrc | 17 - frontend/.env.development | 4 + frontend/.eslintrc | 127 + frontend/.gitignore | 8 +- {backend => frontend}/.prettierrc | 8 +- frontend/README.md | 20 + frontend/check-locales.js | 127 + frontend/fonts/feather | 1 - ...urce-sans-pro-v14-latin-ext_latin-700.woff | Bin 31740 -> 0 bytes ...rce-sans-pro-v14-latin-ext_latin-700.woff2 | Bin 25348 -> 0 bytes ...ans-pro-v14-latin-ext_latin-700italic.woff | Bin 28540 -> 0 bytes ...ns-pro-v14-latin-ext_latin-700italic.woff2 | Bin 22240 -> 0 bytes ...e-sans-pro-v14-latin-ext_latin-italic.woff | Bin 28744 -> 0 bytes ...-sans-pro-v14-latin-ext_latin-italic.woff2 | Bin 22436 -> 0 bytes ...-sans-pro-v14-latin-ext_latin-regular.woff | Bin 32128 -> 0 bytes ...sans-pro-v14-latin-ext_latin-regular.woff2 | Bin 25656 -> 0 bytes frontend/globalSetup.js | 4 + frontend/html/index.ejs | 9 - frontend/html/login.ejs | 9 - frontend/html/partials/footer.ejs | 2 - frontend/html/partials/header.ejs | 33 - frontend/images | 1 - frontend/jest.eslint.js | 6 + frontend/js/app/api.js | 757 - frontend/js/app/audit-log/list/item.ejs | 80 - frontend/js/app/audit-log/list/item.js | 32 - frontend/js/app/audit-log/list/main.ejs | 9 - frontend/js/app/audit-log/list/main.js | 27 - frontend/js/app/audit-log/main.ejs | 25 - frontend/js/app/audit-log/main.js | 82 - frontend/js/app/audit-log/meta.ejs | 27 - frontend/js/app/audit-log/meta.js | 7 - frontend/js/app/cache.js | 10 - frontend/js/app/controller.js | 447 - frontend/js/app/dashboard/main.ejs | 67 - frontend/js/app/dashboard/main.js | 92 - frontend/js/app/empty/main.ejs | 11 - frontend/js/app/empty/main.js | 33 - frontend/js/app/error/main.ejs | 7 - frontend/js/app/error/main.js | 27 - frontend/js/app/help/main.ejs | 12 - frontend/js/app/help/main.js | 16 - frontend/js/app/i18n.js | 23 - frontend/js/app/main.js | 155 - frontend/js/app/nginx/access/delete.ejs | 23 - frontend/js/app/nginx/access/delete.js | 32 - frontend/js/app/nginx/access/form.ejs | 108 - frontend/js/app/nginx/access/form.js | 153 - frontend/js/app/nginx/access/form/client.ejs | 13 - frontend/js/app/nginx/access/form/client.js | 7 - frontend/js/app/nginx/access/form/item.ejs | 10 - frontend/js/app/nginx/access/form/item.js | 7 - frontend/js/app/nginx/access/list/item.ejs | 42 - frontend/js/app/nginx/access/list/item.js | 33 - frontend/js/app/nginx/access/list/main.ejs | 14 - frontend/js/app/nginx/access/list/main.js | 32 - frontend/js/app/nginx/access/main.ejs | 28 - frontend/js/app/nginx/access/main.js | 108 - .../js/app/nginx/certificates-list-item.ejs | 18 - frontend/js/app/nginx/certificates/delete.ejs | 19 - frontend/js/app/nginx/certificates/delete.js | 34 - frontend/js/app/nginx/certificates/form.ejs | 185 - frontend/js/app/nginx/certificates/form.js | 294 - .../js/app/nginx/certificates/list/item.ejs | 54 - .../js/app/nginx/certificates/list/item.js | 58 - .../js/app/nginx/certificates/list/main.ejs | 12 - .../js/app/nginx/certificates/list/main.js | 32 - frontend/js/app/nginx/certificates/main.ejs | 36 - frontend/js/app/nginx/certificates/main.js | 109 - frontend/js/app/nginx/certificates/renew.ejs | 14 - frontend/js/app/nginx/certificates/renew.js | 31 - frontend/js/app/nginx/certificates/test.ejs | 15 - frontend/js/app/nginx/certificates/test.js | 75 - frontend/js/app/nginx/dead/delete.ejs | 23 - frontend/js/app/nginx/dead/delete.js | 32 - frontend/js/app/nginx/dead/form.ejs | 206 - frontend/js/app/nginx/dead/form.js | 286 - frontend/js/app/nginx/dead/list/item.ejs | 54 - frontend/js/app/nginx/dead/list/item.js | 61 - frontend/js/app/nginx/dead/list/main.ejs | 12 - frontend/js/app/nginx/dead/list/main.js | 32 - frontend/js/app/nginx/dead/main.ejs | 28 - frontend/js/app/nginx/dead/main.js | 108 - .../js/app/nginx/proxy/access-list-item.ejs | 13 - frontend/js/app/nginx/proxy/delete.ejs | 23 - frontend/js/app/nginx/proxy/delete.js | 32 - frontend/js/app/nginx/proxy/form.ejs | 281 - frontend/js/app/nginx/proxy/form.js | 369 - frontend/js/app/nginx/proxy/list/item.ejs | 60 - frontend/js/app/nginx/proxy/list/item.js | 61 - frontend/js/app/nginx/proxy/list/main.ejs | 14 - frontend/js/app/nginx/proxy/list/main.js | 32 - frontend/js/app/nginx/proxy/location-item.ejs | 64 - frontend/js/app/nginx/proxy/location.js | 54 - frontend/js/app/nginx/proxy/main.ejs | 28 - frontend/js/app/nginx/proxy/main.js | 108 - frontend/js/app/nginx/redirection/delete.ejs | 23 - frontend/js/app/nginx/redirection/delete.js | 32 - frontend/js/app/nginx/redirection/form.ejs | 253 - frontend/js/app/nginx/redirection/form.js | 288 - .../js/app/nginx/redirection/list/item.ejs | 63 - .../js/app/nginx/redirection/list/item.js | 61 - .../js/app/nginx/redirection/list/main.ejs | 15 - .../js/app/nginx/redirection/list/main.js | 32 - frontend/js/app/nginx/redirection/main.ejs | 28 - frontend/js/app/nginx/redirection/main.js | 107 - frontend/js/app/nginx/stream/delete.ejs | 19 - frontend/js/app/nginx/stream/delete.js | 32 - frontend/js/app/nginx/stream/form.ejs | 55 - frontend/js/app/nginx/stream/form.js | 84 - frontend/js/app/nginx/stream/list/item.ejs | 53 - frontend/js/app/nginx/stream/list/item.js | 54 - frontend/js/app/nginx/stream/list/main.ejs | 13 - frontend/js/app/nginx/stream/list/main.js | 32 - frontend/js/app/nginx/stream/main.ejs | 28 - frontend/js/app/nginx/stream/main.js | 108 - frontend/js/app/router.js | 19 - .../js/app/settings/default-site/main.ejs | 53 - frontend/js/app/settings/default-site/main.js | 69 - frontend/js/app/settings/list/item.ejs | 21 - frontend/js/app/settings/list/item.js | 23 - frontend/js/app/settings/list/main.ejs | 8 - frontend/js/app/settings/list/main.js | 27 - frontend/js/app/settings/main.ejs | 14 - frontend/js/app/settings/main.js | 48 - frontend/js/app/tokens.js | 126 - frontend/js/app/ui/footer/main.ejs | 16 - frontend/js/app/ui/footer/main.js | 14 - frontend/js/app/ui/header/main.ejs | 34 - frontend/js/app/ui/header/main.js | 67 - frontend/js/app/ui/main.ejs | 21 - frontend/js/app/ui/main.js | 98 - frontend/js/app/ui/menu/main.ejs | 52 - frontend/js/app/ui/menu/main.js | 39 - frontend/js/app/user/delete.ejs | 19 - frontend/js/app/user/delete.js | 34 - frontend/js/app/user/form.ejs | 58 - frontend/js/app/user/form.js | 108 - frontend/js/app/user/password.ejs | 31 - frontend/js/app/user/password.js | 69 - frontend/js/app/user/permissions.ejs | 68 - frontend/js/app/user/permissions.js | 95 - frontend/js/app/users/list/item.ejs | 45 - frontend/js/app/users/list/item.js | 68 - frontend/js/app/users/list/main.ejs | 10 - frontend/js/app/users/list/main.js | 27 - frontend/js/app/users/main.ejs | 26 - frontend/js/app/users/main.js | 78 - frontend/js/i18n/messages.json | 294 - frontend/js/index.js | 119 - frontend/js/lib/helpers.js | 26 - frontend/js/lib/marionette.js | 15 - frontend/js/login.js | 5 - frontend/js/login/main.js | 14 - frontend/js/login/ui/login.ejs | 37 - frontend/js/login/ui/login.js | 42 - frontend/js/models/access-list.js | 25 - frontend/js/models/audit-log.js | 18 - frontend/js/models/certificate.js | 38 - frontend/js/models/dead-host.js | 32 - frontend/js/models/proxy-host-location.js | 35 - frontend/js/models/proxy-host.js | 40 - frontend/js/models/redirection-host.js | 37 - frontend/js/models/setting.js | 22 - frontend/js/models/stream.js | 29 - frontend/js/models/user.js | 54 - frontend/package.json | 154 +- .../images}/default-avatar.jpg | Bin .../favicon}/android-chrome-192x192.png | Bin .../favicon}/android-chrome-512x512.png | Bin .../images/favicon}/apple-touch-icon.png | Bin .../images/favicon}/browserconfig.xml | 0 .../images/favicon}/favicon-16x16.png | Bin .../images/favicon}/favicon-32x32.png | Bin .../images/favicon}/favicon.ico | Bin .../images/favicon}/mstile-150x150.png | Bin .../images/favicon}/safari-pinned-tab.svg | 0 .../images/favicon}/site.webmanifest | 0 .../images}/logo-256.png | Bin .../images/logo-bold-horizontal-grey.svg | 1 + frontend/public/images/logo-no-text.svg | 93 + .../images/logo-text-horizontal-grey.png | Bin 0 -> 22203 bytes .../images}/logo-text-vertical-grey.png | Bin frontend/public/index.html | 55 + frontend/scss/custom.scss | 42 - frontend/scss/fonts.scss | 39 - frontend/scss/selectize.scss | 196 - frontend/scss/styles.scss | 17 - frontend/scss/tabler-extra.scss | 170 - frontend/src/App.test.tsx | 11 + frontend/src/App.tsx | 31 + frontend/src/Router.tsx | 81 + frontend/src/api/npm/base.ts | 95 + .../src/api/npm/createCertificateAuthority.ts | 16 + frontend/src/api/npm/createDNSProvider.ts | 16 + frontend/src/api/npm/createUser.ts | 30 + .../src/api/npm/getCertificateAuthorities.ts | 19 + .../src/api/npm/getCertificateAuthority.ts | 13 + frontend/src/api/npm/getCertificates.ts | 19 + frontend/src/api/npm/getDNSProvider.ts | 13 + frontend/src/api/npm/getDNSProviders.ts | 19 + frontend/src/api/npm/getDNSProvidersAcmesh.ts | 14 + frontend/src/api/npm/getHealth.ts | 14 + frontend/src/api/npm/getHostTemplates.ts | 19 + frontend/src/api/npm/getHosts.ts | 19 + frontend/src/api/npm/getSettings.ts | 19 + frontend/src/api/npm/getToken.ts | 24 + frontend/src/api/npm/getUser.ts | 14 + frontend/src/api/npm/getUsers.ts | 19 + frontend/src/api/npm/helpers.ts | 34 + frontend/src/api/npm/index.ts | 24 + frontend/src/api/npm/models.ts | 123 + frontend/src/api/npm/refreshToken.ts | 14 + frontend/src/api/npm/responseTypes.ts | 58 + frontend/src/api/npm/setAuth.ts | 18 + .../src/api/npm/setCertificateAuthority.ts | 17 + frontend/src/api/npm/setDNSProvider.ts | 17 + frontend/src/api/npm/setUser.ts | 18 + frontend/src/components/EmptyList.tsx | 23 + frontend/src/components/Flag/Flag.tsx | 32 + frontend/src/components/Flag/index.ts | 1 + frontend/src/components/Footer.tsx | 64 + .../src/components/HelpDrawer/HelpDrawer.tsx | 56 + frontend/src/components/HelpDrawer/index.ts | 1 + frontend/src/components/Loader/Loader.tsx | 19 + frontend/src/components/Loader/index.ts | 1 + frontend/src/components/Loading.tsx | 12 + frontend/src/components/LocalePicker.tsx | 62 + .../src/components/Navigation/Navigation.tsx | 24 + .../Navigation/NavigationHeader.tsx | 113 + .../components/Navigation/NavigationMenu.tsx | 331 + frontend/src/components/Navigation/index.ts | 3 + .../Permissions/AdminPermissionSelector.tsx | 36 + .../Permissions/PermissionSelector.tsx | 285 + frontend/src/components/Permissions/index.ts | 2 + frontend/src/components/PrettyButton.tsx | 20 + frontend/src/components/SiteWrapper.tsx | 32 + frontend/src/components/SpinnerPage.tsx | 9 + frontend/src/components/Table/Formatters.tsx | 225 + .../src/components/Table/RowActionsMenu.tsx | 70 + frontend/src/components/Table/TableHelpers.ts | 57 + frontend/src/components/Table/TableLayout.tsx | 246 + frontend/src/components/Table/TextFilter.tsx | 142 + frontend/src/components/Table/index.ts | 5 + .../components/Table/react-table-config.d.ts | 130 + frontend/src/components/ThemeSwitcher.tsx | 34 + frontend/src/components/Unhealthy.tsx | 38 + frontend/src/components/index.ts | 15 + frontend/src/context/AuthContext.tsx | 79 + frontend/src/context/LocaleContext.tsx | 41 + frontend/src/context/index.ts | 2 + frontend/src/declarations.d.ts | 1 + .../source-sans-pro-v14-latin-700.woff | Bin 0 -> 19896 bytes .../source-sans-pro-v14-latin-700.woff2 | Bin 0 -> 15764 bytes .../source-sans-pro-v14-latin-700italic.woff | Bin 0 -> 19248 bytes .../source-sans-pro-v14-latin-700italic.woff2 | Bin 0 -> 15188 bytes .../source-sans-pro-v14-latin-italic.woff | Bin 0 -> 19368 bytes .../source-sans-pro-v14-latin-italic.woff2 | Bin 0 -> 15280 bytes .../source-sans-pro-v14-latin-regular.woff | Bin 0 -> 20180 bytes .../source-sans-pro-v14-latin-regular.woff2 | Bin 0 -> 16112 bytes frontend/src/hooks/index.ts | 12 + .../src/hooks/useCertificateAuthorities.ts | 41 + frontend/src/hooks/useCertificateAuthority.ts | 62 + frontend/src/hooks/useCertificates.ts | 41 + frontend/src/hooks/useDNSProvider.ts | 56 + frontend/src/hooks/useDNSProviders.ts | 41 + frontend/src/hooks/useDNSProvidersAcmesh.ts | 16 + frontend/src/hooks/useHealth.ts | 16 + frontend/src/hooks/useHostTemplates.ts | 41 + frontend/src/hooks/useHosts.ts | 36 + frontend/src/hooks/useSettings.ts | 36 + frontend/src/hooks/useUser.ts | 37 + frontend/src/hooks/useUsers.ts | 36 + frontend/src/img/logo-256.png | Bin 0 -> 31064 bytes frontend/src/img/logo-text-vertical-grey.png | Bin 0 -> 14281 bytes frontend/src/index.scss | 59 + frontend/src/index.tsx | 28 + frontend/src/locale/IntlProvider.tsx | 75 + frontend/src/locale/index.ts | 1 + .../src/HelpDoc/de/CertificateAuthorities.md | 3 + frontend/src/locale/src/HelpDoc/de/index.ts | 1 + .../src/HelpDoc/en/CertificateAuthorities.md | 26 + frontend/src/locale/src/HelpDoc/en/index.ts | 1 + .../src/HelpDoc/fa/CertificateAuthorities.md | 3 + frontend/src/locale/src/HelpDoc/fa/index.ts | 1 + frontend/src/locale/src/HelpDoc/index.ts | 17 + frontend/src/locale/src/de.json | 371 + frontend/src/locale/src/en.json | 506 + frontend/src/locale/src/fa.json | 371 + frontend/src/locale/src/lang-list.json | 11 + .../CertificateAuthorityCreateModal.tsx | 221 + .../modals/CertificateAuthorityEditModal.tsx | 240 + frontend/src/modals/ChangePasswordModal.tsx | 174 + .../src/modals/DNSProviderCreateModal.tsx | 226 + frontend/src/modals/ProfileModal.tsx | 156 + frontend/src/modals/SetPasswordModal.tsx | 152 + frontend/src/modals/UserCreateModal.tsx | 275 + frontend/src/modals/UserEditModal.tsx | 272 + frontend/src/modals/index.ts | 8 + frontend/src/modules/Acmesh.ts | 74 + frontend/src/modules/AuthStore.ts | 84 + frontend/src/modules/Validations.tsx | 63 + frontend/src/pages/AccessLists/index.tsx | 10 + frontend/src/pages/AuditLog/index.tsx | 10 + .../pages/CertificateAuthorities/Table.tsx | 155 + .../CertificateAuthorities/TableWrapper.tsx | 74 + .../pages/CertificateAuthorities/index.tsx | 35 + .../pages/Certificates/CertificatesTable.tsx | 152 + frontend/src/pages/Certificates/index.tsx | 99 + frontend/src/pages/DNSProviders/Table.tsx | 152 + .../src/pages/DNSProviders/TableWrapper.tsx | 97 + frontend/src/pages/DNSProviders/index.tsx | 35 + frontend/src/pages/Dashboard/index.tsx | 10 + .../HostTemplates/HostTemplatesTable.tsx | 148 + frontend/src/pages/HostTemplates/index.tsx | 80 + frontend/src/pages/Hosts/HostsTable.tsx | 165 + frontend/src/pages/Hosts/index.tsx | 98 + frontend/src/pages/Login/index.tsx | 167 + frontend/src/pages/Settings/SettingsTable.tsx | 138 + frontend/src/pages/Settings/index.tsx | 73 + frontend/src/pages/Setup/index.tsx | 241 + frontend/src/pages/Users/Table.tsx | 173 + frontend/src/pages/Users/TableWrapper.tsx | 78 + frontend/src/pages/Users/index.tsx | 30 + frontend/src/react-app-env.d.ts | 1 + frontend/src/styles/fonts.scss | 47 + frontend/src/theme/customTheme.ts | 22 + frontend/tsconfig.json | 28 + frontend/webpack.config.js | 144 - frontend/yarn.lock | 14051 +++++++++++----- global/certbot-dns-plugins.js | 515 - scripts/.common.sh | 12 +- scripts/buildx | 8 +- scripts/ci/build-backend | 77 + scripts/ci/build-cleanup | 19 + scripts/ci/build-frontend | 49 + scripts/ci/fulltest-cypress | 105 + scripts/ci/test-backend | 70 + scripts/docs-build | 2 +- scripts/frontend-build | 17 - scripts/frontend-lint | 17 + scripts/go-multiarch-wrapper | 33 + scripts/install-s6 | 7 +- scripts/sqlite | 17 + scripts/start-dev | 48 +- scripts/test-dev | 2 +- scripts/wait-healthy | 21 +- test/.dockerignore | 1 - test/.gitignore | 6 +- test/cypress/Dockerfile | 9 +- .../integration/api/Certificates.spec.js | 119 + .../integration/api/FullCertProvision.spec.js | 95 + test/cypress/integration/api/Health.spec.js | 12 +- test/cypress/integration/api/Settings.spec.js | 118 + .../integration/api/SetupPhase.spec.js | 25 + .../integration/api/SwaggerSchema.spec.js | 9 + test/cypress/integration/api/Users.spec.js | 160 +- .../cypress/integration/ui/SetupLogin.spec.js | 38 + test/cypress/plugins/backendApi/client.js | 40 +- test/cypress/plugins/backendApi/task.js | 2 +- test/cypress/plugins/index.js | 2 +- test/cypress/support/commands.js | 113 +- test/cypress/support/index.js | 2 - test/package.json | 20 +- test/yarn.lock | 2091 +-- 830 files changed, 38168 insertions(+), 36635 deletions(-) create mode 100644 .dockerignore create mode 100644 DEV-README.md create mode 100644 backend/.editorconfig delete mode 100644 backend/.eslintrc.json delete mode 100644 backend/.gitignore create mode 100644 backend/.golangci.yml create mode 100644 backend/.nancy-ignore delete mode 100644 backend/.vscode/settings.json create mode 100644 backend/README.md create mode 100644 backend/Taskfile.yml delete mode 100644 backend/app.js create mode 100644 backend/cmd/server/main.go delete mode 100644 backend/config/README.md delete mode 100644 backend/config/default.json delete mode 100644 backend/config/sqlite-test-db.json delete mode 100644 backend/db.js delete mode 100644 backend/doc/api.swagger.json create mode 100644 backend/embed/api_docs/api.swagger.json create mode 100644 backend/embed/api_docs/components/CertificateAuthorityList.json create mode 100644 backend/embed/api_docs/components/CertificateAuthorityObject.json create mode 100644 backend/embed/api_docs/components/CertificateList.json create mode 100644 backend/embed/api_docs/components/CertificateObject.json create mode 100644 backend/embed/api_docs/components/ConfigObject.json create mode 100644 backend/embed/api_docs/components/DNSProviderList.json create mode 100644 backend/embed/api_docs/components/DNSProviderObject.json create mode 100644 backend/embed/api_docs/components/DeletedItemResponse.json create mode 100644 backend/embed/api_docs/components/ErrorObject.json create mode 100644 backend/embed/api_docs/components/FilterObject.json create mode 100644 backend/embed/api_docs/components/HealthObject.json create mode 100644 backend/embed/api_docs/components/HostList.json create mode 100644 backend/embed/api_docs/components/HostObject.json create mode 100644 backend/embed/api_docs/components/HostTemplateList.json create mode 100644 backend/embed/api_docs/components/HostTemplateObject.json create mode 100644 backend/embed/api_docs/components/SettingList.json create mode 100644 backend/embed/api_docs/components/SettingObject.json create mode 100644 backend/embed/api_docs/components/SortObject.json create mode 100644 backend/embed/api_docs/components/StreamList.json create mode 100644 backend/embed/api_docs/components/StreamObject.json create mode 100644 backend/embed/api_docs/components/TokenObject.json create mode 100644 backend/embed/api_docs/components/UserAuthObject.json create mode 100644 backend/embed/api_docs/components/UserList.json create mode 100644 backend/embed/api_docs/components/UserObject.json create mode 100644 backend/embed/api_docs/main.go create mode 100644 backend/embed/api_docs/paths/certificates-authorities/caID/delete.json create mode 100644 backend/embed/api_docs/paths/certificates-authorities/caID/get.json create mode 100644 backend/embed/api_docs/paths/certificates-authorities/caID/put.json create mode 100644 backend/embed/api_docs/paths/certificates-authorities/get.json create mode 100644 backend/embed/api_docs/paths/certificates-authorities/post.json create mode 100644 backend/embed/api_docs/paths/certificates/certificateID/delete.json create mode 100644 backend/embed/api_docs/paths/certificates/certificateID/get.json create mode 100644 backend/embed/api_docs/paths/certificates/certificateID/put.json create mode 100644 backend/embed/api_docs/paths/certificates/get.json create mode 100644 backend/embed/api_docs/paths/certificates/post.json create mode 100644 backend/embed/api_docs/paths/config/get.json create mode 100644 backend/embed/api_docs/paths/dns-providers/get.json create mode 100644 backend/embed/api_docs/paths/dns-providers/post.json create mode 100644 backend/embed/api_docs/paths/dns-providers/providerID/delete.json create mode 100644 backend/embed/api_docs/paths/dns-providers/providerID/get.json create mode 100644 backend/embed/api_docs/paths/dns-providers/providerID/put.json create mode 100644 backend/embed/api_docs/paths/get.json create mode 100644 backend/embed/api_docs/paths/host-templates/get.json create mode 100644 backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json create mode 100644 backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json create mode 100644 backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json create mode 100644 backend/embed/api_docs/paths/host-templates/post.json create mode 100644 backend/embed/api_docs/paths/hosts/get.json create mode 100644 backend/embed/api_docs/paths/hosts/hostID/delete.json create mode 100644 backend/embed/api_docs/paths/hosts/hostID/get.json create mode 100644 backend/embed/api_docs/paths/hosts/hostID/put.json create mode 100644 backend/embed/api_docs/paths/hosts/post.json create mode 100644 backend/embed/api_docs/paths/schema/get.json create mode 100644 backend/embed/api_docs/paths/settings/get.json create mode 100644 backend/embed/api_docs/paths/settings/name/get.json create mode 100644 backend/embed/api_docs/paths/settings/name/put.json create mode 100644 backend/embed/api_docs/paths/settings/post.json create mode 100644 backend/embed/api_docs/paths/streams/get.json create mode 100644 backend/embed/api_docs/paths/streams/post.json create mode 100644 backend/embed/api_docs/paths/streams/streamID/delete.json create mode 100644 backend/embed/api_docs/paths/streams/streamID/get.json create mode 100644 backend/embed/api_docs/paths/streams/streamID/put.json create mode 100644 backend/embed/api_docs/paths/tokens/get.json create mode 100644 backend/embed/api_docs/paths/tokens/post.json create mode 100644 backend/embed/api_docs/paths/users/get.json create mode 100644 backend/embed/api_docs/paths/users/post.json create mode 100644 backend/embed/api_docs/paths/users/userID/auth/post.json create mode 100644 backend/embed/api_docs/paths/users/userID/delete.json create mode 100644 backend/embed/api_docs/paths/users/userID/get.json create mode 100644 backend/embed/api_docs/paths/users/userID/put.json create mode 100644 backend/embed/main.go create mode 100644 backend/embed/migrations/20201013035318_initial_schema.sql create mode 100644 backend/embed/migrations/20201013035839_initial_data.sql create mode 100644 backend/embed/nginx/_assets.conf.hbs create mode 100644 backend/embed/nginx/_certificates.conf.hbs create mode 100644 backend/embed/nginx/_forced_ssl.conf.hbs create mode 100644 backend/embed/nginx/_hsts.conf.hbs create mode 100644 backend/embed/nginx/_listen.conf.hbs create mode 100644 backend/embed/nginx/_location.conf.hbs create mode 100644 backend/embed/nginx/acme-request.conf.hbs create mode 100644 backend/embed/nginx/dead_host.conf.hbs create mode 100644 backend/embed/nginx/default.conf.hbs create mode 100644 backend/embed/nginx/ip_ranges.conf.hbs create mode 100644 backend/embed/nginx/proxy_host.conf.hbs create mode 100644 backend/embed/nginx/redirection_host.conf.hbs create mode 100644 backend/embed/nginx/stream.conf.hbs create mode 100644 backend/go.mod create mode 100644 backend/go.sum delete mode 100644 backend/index.js delete mode 100644 backend/internal/access-list.js create mode 100644 backend/internal/acme/acmesh.go create mode 100644 backend/internal/acme/acmesh_test.go create mode 100644 backend/internal/acme/errors.go create mode 100644 backend/internal/api/context/context.go create mode 100644 backend/internal/api/filters/helpers.go create mode 100644 backend/internal/api/handler/auth.go create mode 100644 backend/internal/api/handler/certificate_authorities.go create mode 100644 backend/internal/api/handler/certificates.go create mode 100644 backend/internal/api/handler/config.go create mode 100644 backend/internal/api/handler/dns_providers.go create mode 100644 backend/internal/api/handler/health.go create mode 100644 backend/internal/api/handler/helpers.go create mode 100644 backend/internal/api/handler/host_templates.go create mode 100644 backend/internal/api/handler/hosts.go create mode 100644 backend/internal/api/handler/not_allowed.go create mode 100644 backend/internal/api/handler/not_found.go create mode 100644 backend/internal/api/handler/schema.go create mode 100644 backend/internal/api/handler/settings.go create mode 100644 backend/internal/api/handler/streams.go create mode 100644 backend/internal/api/handler/tokens.go create mode 100644 backend/internal/api/handler/users.go create mode 100644 backend/internal/api/http/requests.go create mode 100644 backend/internal/api/http/responses.go create mode 100644 backend/internal/api/middleware/access_control.go create mode 100644 backend/internal/api/middleware/auth.go create mode 100644 backend/internal/api/middleware/auth_cache.go create mode 100644 backend/internal/api/middleware/body_context.go create mode 100644 backend/internal/api/middleware/cors.go create mode 100644 backend/internal/api/middleware/enforce_setup.go create mode 100644 backend/internal/api/middleware/expansion.go create mode 100644 backend/internal/api/middleware/filters.go create mode 100644 backend/internal/api/middleware/pretty_print.go create mode 100644 backend/internal/api/middleware/schema.go create mode 100644 backend/internal/api/router.go create mode 100644 backend/internal/api/router_test.go create mode 100644 backend/internal/api/schema/certificates.go create mode 100644 backend/internal/api/schema/common.go create mode 100644 backend/internal/api/schema/create_certificate_authority.go create mode 100644 backend/internal/api/schema/create_dns_provider.go create mode 100644 backend/internal/api/schema/create_host.go create mode 100644 backend/internal/api/schema/create_host_template.go create mode 100644 backend/internal/api/schema/create_setting.go create mode 100644 backend/internal/api/schema/create_stream.go create mode 100644 backend/internal/api/schema/create_user.go create mode 100644 backend/internal/api/schema/get_token.go create mode 100644 backend/internal/api/schema/set_auth.go create mode 100644 backend/internal/api/schema/update_certificate_authority.go create mode 100644 backend/internal/api/schema/update_dns_provider.go create mode 100644 backend/internal/api/schema/update_host.go create mode 100644 backend/internal/api/schema/update_host_template.go create mode 100644 backend/internal/api/schema/update_setting.go create mode 100644 backend/internal/api/schema/update_stream.go create mode 100644 backend/internal/api/schema/update_user.go create mode 100644 backend/internal/api/server.go delete mode 100644 backend/internal/audit-log.js create mode 100644 backend/internal/cache/cache.go delete mode 100644 backend/internal/certificate.js create mode 100644 backend/internal/config/args.go create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/config/folders.go create mode 100644 backend/internal/config/keys.go create mode 100644 backend/internal/config/vars.go create mode 100644 backend/internal/database/helpers.go create mode 100644 backend/internal/database/migrator.go create mode 100644 backend/internal/database/setup.go create mode 100644 backend/internal/database/sqlite.go delete mode 100644 backend/internal/dead-host.js create mode 100644 backend/internal/dnsproviders/common.go create mode 100644 backend/internal/dnsproviders/dns_ad.go create mode 100644 backend/internal/dnsproviders/dns_ali.go create mode 100644 backend/internal/dnsproviders/dns_aws.go create mode 100644 backend/internal/dnsproviders/dns_cf.go create mode 100644 backend/internal/dnsproviders/dns_cloudns.go create mode 100644 backend/internal/dnsproviders/dns_cx.go create mode 100644 backend/internal/dnsproviders/dns_cyon.go create mode 100644 backend/internal/dnsproviders/dns_dgon.go create mode 100644 backend/internal/dnsproviders/dns_dnsimple.go create mode 100644 backend/internal/dnsproviders/dns_dp.go create mode 100644 backend/internal/dnsproviders/dns_duckdns.go create mode 100644 backend/internal/dnsproviders/dns_dyn.go create mode 100644 backend/internal/dnsproviders/dns_dynu.go create mode 100644 backend/internal/dnsproviders/dns_freedns.go create mode 100644 backend/internal/dnsproviders/dns_gandi_livedns.go create mode 100644 backend/internal/dnsproviders/dns_gd.go create mode 100644 backend/internal/dnsproviders/dns_he.go create mode 100644 backend/internal/dnsproviders/dns_infoblox.go create mode 100644 backend/internal/dnsproviders/dns_ispconfig.go create mode 100644 backend/internal/dnsproviders/dns_linode_v4.go create mode 100644 backend/internal/dnsproviders/dns_lua.go create mode 100644 backend/internal/dnsproviders/dns_me.go create mode 100644 backend/internal/dnsproviders/dns_namecom.go create mode 100644 backend/internal/dnsproviders/dns_nsone.go create mode 100644 backend/internal/dnsproviders/dns_pdns.go create mode 100644 backend/internal/dnsproviders/dns_unoeuro.go create mode 100644 backend/internal/dnsproviders/dns_vscale.go create mode 100644 backend/internal/dnsproviders/dns_yandex.go create mode 100644 backend/internal/entity/auth/methods.go create mode 100644 backend/internal/entity/auth/model.go create mode 100644 backend/internal/entity/certificate/filters.go create mode 100644 backend/internal/entity/certificate/methods.go create mode 100644 backend/internal/entity/certificate/model.go create mode 100644 backend/internal/entity/certificate/structs.go create mode 100644 backend/internal/entity/certificateauthority/filters.go create mode 100644 backend/internal/entity/certificateauthority/methods.go create mode 100644 backend/internal/entity/certificateauthority/model.go create mode 100644 backend/internal/entity/certificateauthority/structs.go create mode 100644 backend/internal/entity/dnsprovider/filters.go create mode 100644 backend/internal/entity/dnsprovider/methods.go create mode 100644 backend/internal/entity/dnsprovider/model.go create mode 100644 backend/internal/entity/dnsprovider/model_test.go create mode 100644 backend/internal/entity/dnsprovider/structs.go create mode 100644 backend/internal/entity/filters.go create mode 100644 backend/internal/entity/filters_schema.go create mode 100644 backend/internal/entity/host/filters.go create mode 100644 backend/internal/entity/host/methods.go create mode 100644 backend/internal/entity/host/model.go create mode 100644 backend/internal/entity/host/structs.go create mode 100644 backend/internal/entity/hosttemplate/filters.go create mode 100644 backend/internal/entity/hosttemplate/methods.go create mode 100644 backend/internal/entity/hosttemplate/model.go create mode 100644 backend/internal/entity/hosttemplate/structs.go create mode 100644 backend/internal/entity/lists_query.go create mode 100644 backend/internal/entity/setting/apply.go create mode 100644 backend/internal/entity/setting/filters.go create mode 100644 backend/internal/entity/setting/methods.go create mode 100644 backend/internal/entity/setting/model.go create mode 100644 backend/internal/entity/setting/structs.go create mode 100644 backend/internal/entity/stream/filters.go create mode 100644 backend/internal/entity/stream/methods.go create mode 100644 backend/internal/entity/stream/model.go create mode 100644 backend/internal/entity/stream/structs.go create mode 100644 backend/internal/entity/user/capabilities.go create mode 100644 backend/internal/entity/user/filters.go create mode 100644 backend/internal/entity/user/methods.go create mode 100644 backend/internal/entity/user/model.go create mode 100644 backend/internal/entity/user/structs.go create mode 100644 backend/internal/errors/errors.go delete mode 100644 backend/internal/host.js delete mode 100644 backend/internal/ip_ranges.js create mode 100644 backend/internal/jwt/jwt.go create mode 100644 backend/internal/jwt/keys.go create mode 100644 backend/internal/logger/config.go create mode 100644 backend/internal/logger/logger.go create mode 100644 backend/internal/logger/logger_test.go create mode 100644 backend/internal/model/filter.go create mode 100644 backend/internal/model/pageinfo.go delete mode 100644 backend/internal/nginx.js create mode 100644 backend/internal/nginx/templates.go delete mode 100644 backend/internal/proxy-host.js delete mode 100644 backend/internal/redirection-host.js delete mode 100644 backend/internal/report.js delete mode 100644 backend/internal/setting.js create mode 100644 backend/internal/state/state.go delete mode 100644 backend/internal/stream.js delete mode 100644 backend/internal/token.js create mode 100644 backend/internal/types/db_date.go create mode 100644 backend/internal/types/jsonb.go create mode 100644 backend/internal/types/nullable_db_date.go delete mode 100644 backend/internal/user.js create mode 100644 backend/internal/util/interfaces.go create mode 100644 backend/internal/util/maps.go create mode 100644 backend/internal/util/maps_test.go create mode 100644 backend/internal/util/slices.go create mode 100644 backend/internal/util/slices_test.go create mode 100644 backend/internal/validator/hosts.go create mode 100644 backend/internal/worker/certificate.go delete mode 100644 backend/knexfile.js delete mode 100644 backend/lib/access.js delete mode 100644 backend/lib/access/access_lists-create.json delete mode 100644 backend/lib/access/access_lists-delete.json delete mode 100644 backend/lib/access/access_lists-get.json delete mode 100644 backend/lib/access/access_lists-list.json delete mode 100644 backend/lib/access/access_lists-update.json delete mode 100644 backend/lib/access/auditlog-list.json delete mode 100644 backend/lib/access/certificates-create.json delete mode 100644 backend/lib/access/certificates-delete.json delete mode 100644 backend/lib/access/certificates-get.json delete mode 100644 backend/lib/access/certificates-list.json delete mode 100644 backend/lib/access/certificates-update.json delete mode 100644 backend/lib/access/dead_hosts-create.json delete mode 100644 backend/lib/access/dead_hosts-delete.json delete mode 100644 backend/lib/access/dead_hosts-get.json delete mode 100644 backend/lib/access/dead_hosts-list.json delete mode 100644 backend/lib/access/dead_hosts-update.json delete mode 100644 backend/lib/access/permissions.json delete mode 100644 backend/lib/access/proxy_hosts-create.json delete mode 100644 backend/lib/access/proxy_hosts-delete.json delete mode 100644 backend/lib/access/proxy_hosts-get.json delete mode 100644 backend/lib/access/proxy_hosts-list.json delete mode 100644 backend/lib/access/proxy_hosts-update.json delete mode 100644 backend/lib/access/redirection_hosts-create.json delete mode 100644 backend/lib/access/redirection_hosts-delete.json delete mode 100644 backend/lib/access/redirection_hosts-get.json delete mode 100644 backend/lib/access/redirection_hosts-list.json delete mode 100644 backend/lib/access/redirection_hosts-update.json delete mode 100644 backend/lib/access/reports-hosts.json delete mode 100644 backend/lib/access/roles.json delete mode 100644 backend/lib/access/settings-get.json delete mode 100644 backend/lib/access/settings-list.json delete mode 100644 backend/lib/access/settings-update.json delete mode 100644 backend/lib/access/streams-create.json delete mode 100644 backend/lib/access/streams-delete.json delete mode 100644 backend/lib/access/streams-get.json delete mode 100644 backend/lib/access/streams-list.json delete mode 100644 backend/lib/access/streams-update.json delete mode 100644 backend/lib/access/users-create.json delete mode 100644 backend/lib/access/users-delete.json delete mode 100644 backend/lib/access/users-get.json delete mode 100644 backend/lib/access/users-list.json delete mode 100644 backend/lib/access/users-loginas.json delete mode 100644 backend/lib/access/users-password.json delete mode 100644 backend/lib/access/users-permissions.json delete mode 100644 backend/lib/access/users-update.json delete mode 100644 backend/lib/error.js delete mode 100644 backend/lib/express/cors.js delete mode 100644 backend/lib/express/jwt-decode.js delete mode 100644 backend/lib/express/jwt.js delete mode 100644 backend/lib/express/pagination.js delete mode 100644 backend/lib/express/user-id-from-me.js delete mode 100644 backend/lib/helpers.js delete mode 100644 backend/lib/migrate_template.js delete mode 100644 backend/lib/utils.js delete mode 100644 backend/lib/validator/api.js delete mode 100644 backend/lib/validator/index.js delete mode 100644 backend/logger.js delete mode 100644 backend/migrate.js delete mode 100644 backend/migrations/20180618015850_initial.js delete mode 100644 backend/migrations/20180929054513_websockets.js delete mode 100644 backend/migrations/20181019052346_forward_host.js delete mode 100644 backend/migrations/20181113041458_http2_support.js delete mode 100644 backend/migrations/20181213013211_forward_scheme.js delete mode 100644 backend/migrations/20190104035154_disabled.js delete mode 100644 backend/migrations/20190215115310_customlocations.js delete mode 100644 backend/migrations/20190218060101_hsts.js delete mode 100644 backend/migrations/20190227065017_settings.js delete mode 100644 backend/migrations/20200410143839_access_list_client.js delete mode 100644 backend/migrations/20200410143840_access_list_client_fix.js delete mode 100644 backend/migrations/20201014143841_pass_auth.js delete mode 100644 backend/migrations/20210210154702_redirection_scheme.js delete mode 100644 backend/migrations/20210210154703_redirection_status_code.js delete mode 100644 backend/migrations/20210423103500_stream_domain.js delete mode 100644 backend/migrations/20211108145214_regenerate_default_host.js delete mode 100644 backend/models/access_list.js delete mode 100644 backend/models/access_list_auth.js delete mode 100644 backend/models/access_list_client.js delete mode 100644 backend/models/audit-log.js delete mode 100644 backend/models/auth.js delete mode 100644 backend/models/certificate.js delete mode 100644 backend/models/dead_host.js delete mode 100644 backend/models/now_helper.js delete mode 100644 backend/models/proxy_host.js delete mode 100644 backend/models/redirection_host.js delete mode 100644 backend/models/setting.js delete mode 100644 backend/models/stream.js delete mode 100644 backend/models/token.js delete mode 100644 backend/models/user.js delete mode 100644 backend/models/user_permission.js delete mode 100644 backend/nodemon.json delete mode 100644 backend/package.json delete mode 100644 backend/routes/api/audit-log.js delete mode 100644 backend/routes/api/main.js delete mode 100644 backend/routes/api/nginx/access_lists.js delete mode 100644 backend/routes/api/nginx/certificates.js delete mode 100644 backend/routes/api/nginx/dead_hosts.js delete mode 100644 backend/routes/api/nginx/proxy_hosts.js delete mode 100644 backend/routes/api/nginx/redirection_hosts.js delete mode 100644 backend/routes/api/nginx/streams.js delete mode 100644 backend/routes/api/reports.js delete mode 100644 backend/routes/api/schema.js delete mode 100644 backend/routes/api/settings.js delete mode 100644 backend/routes/api/tokens.js delete mode 100644 backend/routes/api/users.js delete mode 100644 backend/schema/definitions.json delete mode 100644 backend/schema/endpoints/access-lists.json delete mode 100644 backend/schema/endpoints/certificates.json delete mode 100644 backend/schema/endpoints/dead-hosts.json delete mode 100644 backend/schema/endpoints/proxy-hosts.json delete mode 100644 backend/schema/endpoints/redirection-hosts.json delete mode 100644 backend/schema/endpoints/settings.json delete mode 100644 backend/schema/endpoints/streams.json delete mode 100644 backend/schema/endpoints/tokens.json delete mode 100644 backend/schema/endpoints/users.json delete mode 100644 backend/schema/examples.json delete mode 100644 backend/schema/index.json create mode 100755 backend/scripts/lint.sh create mode 100755 backend/scripts/test.sh delete mode 100644 backend/setup.js delete mode 100644 backend/templates/_assets.conf delete mode 100644 backend/templates/_certificates.conf delete mode 100644 backend/templates/_exploits.conf delete mode 100644 backend/templates/_forced_ssl.conf delete mode 100644 backend/templates/_header_comment.conf delete mode 100644 backend/templates/_hsts.conf delete mode 100644 backend/templates/_listen.conf delete mode 100644 backend/templates/_location.conf delete mode 100644 backend/templates/dead_host.conf delete mode 100644 backend/templates/default.conf delete mode 100644 backend/templates/ip_ranges.conf delete mode 100644 backend/templates/letsencrypt-request.conf delete mode 100644 backend/templates/proxy_host.conf delete mode 100644 backend/templates/redirection_host.conf delete mode 100644 backend/templates/stream.conf delete mode 100644 backend/yarn.lock create mode 100644 docker/dev/dnsrouter-config.json create mode 100644 docker/dev/pdns-db.sql create mode 100644 docker/dev/pebble-config.json delete mode 100755 docker/rootfs/bin/check-health delete mode 100755 docker/rootfs/bin/handle-ipv6-setting create mode 100755 docker/rootfs/bin/healthcheck.sh delete mode 100644 docker/rootfs/etc/cont-init.d/.gitignore delete mode 100755 docker/rootfs/etc/cont-init.d/01_perms.sh delete mode 100644 docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh create mode 100755 docker/rootfs/etc/cont-init.d/10-nginx create mode 100755 docker/rootfs/etc/cont-init.d/20-adduser delete mode 100644 docker/rootfs/etc/letsencrypt.ini delete mode 100644 docker/rootfs/etc/logrotate.d/nginx-proxy-manager delete mode 100644 docker/rootfs/etc/nginx/conf.d/include/.gitignore create mode 100644 docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf delete mode 100644 docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf delete mode 100644 docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf create mode 100644 docker/rootfs/etc/nginx/conf.d/include/resolvers.conf create mode 100755 docker/rootfs/etc/services.d/backend/finish create mode 100755 docker/rootfs/etc/services.d/backend/run delete mode 100755 docker/rootfs/etc/services.d/manager/finish delete mode 100755 docker/rootfs/etc/services.d/manager/run mode change 120000 => 100755 docker/rootfs/etc/services.d/nginx/finish create mode 100644 docker/rootfs/root/.config/litecli/config delete mode 100644 frontend/.babelrc create mode 100644 frontend/.env.development create mode 100644 frontend/.eslintrc rename {backend => frontend}/.prettierrc (55%) create mode 100644 frontend/README.md create mode 100755 frontend/check-locales.js delete mode 120000 frontend/fonts/feather delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff delete mode 100644 frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 create mode 100644 frontend/globalSetup.js delete mode 100644 frontend/html/index.ejs delete mode 100644 frontend/html/login.ejs delete mode 100644 frontend/html/partials/footer.ejs delete mode 100644 frontend/html/partials/header.ejs delete mode 120000 frontend/images create mode 100644 frontend/jest.eslint.js delete mode 100644 frontend/js/app/api.js delete mode 100644 frontend/js/app/audit-log/list/item.ejs delete mode 100644 frontend/js/app/audit-log/list/item.js delete mode 100644 frontend/js/app/audit-log/list/main.ejs delete mode 100644 frontend/js/app/audit-log/list/main.js delete mode 100644 frontend/js/app/audit-log/main.ejs delete mode 100644 frontend/js/app/audit-log/main.js delete mode 100644 frontend/js/app/audit-log/meta.ejs delete mode 100644 frontend/js/app/audit-log/meta.js delete mode 100644 frontend/js/app/cache.js delete mode 100644 frontend/js/app/controller.js delete mode 100644 frontend/js/app/dashboard/main.ejs delete mode 100644 frontend/js/app/dashboard/main.js delete mode 100644 frontend/js/app/empty/main.ejs delete mode 100644 frontend/js/app/empty/main.js delete mode 100644 frontend/js/app/error/main.ejs delete mode 100644 frontend/js/app/error/main.js delete mode 100644 frontend/js/app/help/main.ejs delete mode 100644 frontend/js/app/help/main.js delete mode 100644 frontend/js/app/i18n.js delete mode 100644 frontend/js/app/main.js delete mode 100644 frontend/js/app/nginx/access/delete.ejs delete mode 100644 frontend/js/app/nginx/access/delete.js delete mode 100644 frontend/js/app/nginx/access/form.ejs delete mode 100644 frontend/js/app/nginx/access/form.js delete mode 100644 frontend/js/app/nginx/access/form/client.ejs delete mode 100644 frontend/js/app/nginx/access/form/client.js delete mode 100644 frontend/js/app/nginx/access/form/item.ejs delete mode 100644 frontend/js/app/nginx/access/form/item.js delete mode 100644 frontend/js/app/nginx/access/list/item.ejs delete mode 100644 frontend/js/app/nginx/access/list/item.js delete mode 100644 frontend/js/app/nginx/access/list/main.ejs delete mode 100644 frontend/js/app/nginx/access/list/main.js delete mode 100644 frontend/js/app/nginx/access/main.ejs delete mode 100644 frontend/js/app/nginx/access/main.js delete mode 100644 frontend/js/app/nginx/certificates-list-item.ejs delete mode 100644 frontend/js/app/nginx/certificates/delete.ejs delete mode 100644 frontend/js/app/nginx/certificates/delete.js delete mode 100644 frontend/js/app/nginx/certificates/form.ejs delete mode 100644 frontend/js/app/nginx/certificates/form.js delete mode 100644 frontend/js/app/nginx/certificates/list/item.ejs delete mode 100644 frontend/js/app/nginx/certificates/list/item.js delete mode 100644 frontend/js/app/nginx/certificates/list/main.ejs delete mode 100644 frontend/js/app/nginx/certificates/list/main.js delete mode 100644 frontend/js/app/nginx/certificates/main.ejs delete mode 100644 frontend/js/app/nginx/certificates/main.js delete mode 100644 frontend/js/app/nginx/certificates/renew.ejs delete mode 100644 frontend/js/app/nginx/certificates/renew.js delete mode 100644 frontend/js/app/nginx/certificates/test.ejs delete mode 100644 frontend/js/app/nginx/certificates/test.js delete mode 100644 frontend/js/app/nginx/dead/delete.ejs delete mode 100644 frontend/js/app/nginx/dead/delete.js delete mode 100644 frontend/js/app/nginx/dead/form.ejs delete mode 100644 frontend/js/app/nginx/dead/form.js delete mode 100644 frontend/js/app/nginx/dead/list/item.ejs delete mode 100644 frontend/js/app/nginx/dead/list/item.js delete mode 100644 frontend/js/app/nginx/dead/list/main.ejs delete mode 100644 frontend/js/app/nginx/dead/list/main.js delete mode 100644 frontend/js/app/nginx/dead/main.ejs delete mode 100644 frontend/js/app/nginx/dead/main.js delete mode 100644 frontend/js/app/nginx/proxy/access-list-item.ejs delete mode 100644 frontend/js/app/nginx/proxy/delete.ejs delete mode 100644 frontend/js/app/nginx/proxy/delete.js delete mode 100644 frontend/js/app/nginx/proxy/form.ejs delete mode 100644 frontend/js/app/nginx/proxy/form.js delete mode 100644 frontend/js/app/nginx/proxy/list/item.ejs delete mode 100644 frontend/js/app/nginx/proxy/list/item.js delete mode 100644 frontend/js/app/nginx/proxy/list/main.ejs delete mode 100644 frontend/js/app/nginx/proxy/list/main.js delete mode 100644 frontend/js/app/nginx/proxy/location-item.ejs delete mode 100644 frontend/js/app/nginx/proxy/location.js delete mode 100644 frontend/js/app/nginx/proxy/main.ejs delete mode 100644 frontend/js/app/nginx/proxy/main.js delete mode 100644 frontend/js/app/nginx/redirection/delete.ejs delete mode 100644 frontend/js/app/nginx/redirection/delete.js delete mode 100644 frontend/js/app/nginx/redirection/form.ejs delete mode 100644 frontend/js/app/nginx/redirection/form.js delete mode 100644 frontend/js/app/nginx/redirection/list/item.ejs delete mode 100644 frontend/js/app/nginx/redirection/list/item.js delete mode 100644 frontend/js/app/nginx/redirection/list/main.ejs delete mode 100644 frontend/js/app/nginx/redirection/list/main.js delete mode 100644 frontend/js/app/nginx/redirection/main.ejs delete mode 100644 frontend/js/app/nginx/redirection/main.js delete mode 100644 frontend/js/app/nginx/stream/delete.ejs delete mode 100644 frontend/js/app/nginx/stream/delete.js delete mode 100644 frontend/js/app/nginx/stream/form.ejs delete mode 100644 frontend/js/app/nginx/stream/form.js delete mode 100644 frontend/js/app/nginx/stream/list/item.ejs delete mode 100644 frontend/js/app/nginx/stream/list/item.js delete mode 100644 frontend/js/app/nginx/stream/list/main.ejs delete mode 100644 frontend/js/app/nginx/stream/list/main.js delete mode 100644 frontend/js/app/nginx/stream/main.ejs delete mode 100644 frontend/js/app/nginx/stream/main.js delete mode 100644 frontend/js/app/router.js delete mode 100644 frontend/js/app/settings/default-site/main.ejs delete mode 100644 frontend/js/app/settings/default-site/main.js delete mode 100644 frontend/js/app/settings/list/item.ejs delete mode 100644 frontend/js/app/settings/list/item.js delete mode 100644 frontend/js/app/settings/list/main.ejs delete mode 100644 frontend/js/app/settings/list/main.js delete mode 100644 frontend/js/app/settings/main.ejs delete mode 100644 frontend/js/app/settings/main.js delete mode 100644 frontend/js/app/tokens.js delete mode 100644 frontend/js/app/ui/footer/main.ejs delete mode 100644 frontend/js/app/ui/footer/main.js delete mode 100644 frontend/js/app/ui/header/main.ejs delete mode 100644 frontend/js/app/ui/header/main.js delete mode 100644 frontend/js/app/ui/main.ejs delete mode 100644 frontend/js/app/ui/main.js delete mode 100644 frontend/js/app/ui/menu/main.ejs delete mode 100644 frontend/js/app/ui/menu/main.js delete mode 100644 frontend/js/app/user/delete.ejs delete mode 100644 frontend/js/app/user/delete.js delete mode 100644 frontend/js/app/user/form.ejs delete mode 100644 frontend/js/app/user/form.js delete mode 100644 frontend/js/app/user/password.ejs delete mode 100644 frontend/js/app/user/password.js delete mode 100644 frontend/js/app/user/permissions.ejs delete mode 100644 frontend/js/app/user/permissions.js delete mode 100644 frontend/js/app/users/list/item.ejs delete mode 100644 frontend/js/app/users/list/item.js delete mode 100644 frontend/js/app/users/list/main.ejs delete mode 100644 frontend/js/app/users/list/main.js delete mode 100644 frontend/js/app/users/main.ejs delete mode 100644 frontend/js/app/users/main.js delete mode 100644 frontend/js/i18n/messages.json delete mode 100644 frontend/js/index.js delete mode 100644 frontend/js/lib/helpers.js delete mode 100644 frontend/js/lib/marionette.js delete mode 100644 frontend/js/login.js delete mode 100644 frontend/js/login/main.js delete mode 100644 frontend/js/login/ui/login.ejs delete mode 100644 frontend/js/login/ui/login.js delete mode 100644 frontend/js/models/access-list.js delete mode 100644 frontend/js/models/audit-log.js delete mode 100644 frontend/js/models/certificate.js delete mode 100644 frontend/js/models/dead-host.js delete mode 100644 frontend/js/models/proxy-host-location.js delete mode 100644 frontend/js/models/proxy-host.js delete mode 100644 frontend/js/models/redirection-host.js delete mode 100644 frontend/js/models/setting.js delete mode 100644 frontend/js/models/stream.js delete mode 100644 frontend/js/models/user.js rename frontend/{app-images => public/images}/default-avatar.jpg (100%) rename frontend/{app-images/favicons => public/images/favicon}/android-chrome-192x192.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/android-chrome-512x512.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/apple-touch-icon.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/browserconfig.xml (100%) rename frontend/{app-images/favicons => public/images/favicon}/favicon-16x16.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/favicon-32x32.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/favicon.ico (100%) rename frontend/{app-images/favicons => public/images/favicon}/mstile-150x150.png (100%) rename frontend/{app-images/favicons => public/images/favicon}/safari-pinned-tab.svg (100%) rename frontend/{app-images/favicons => public/images/favicon}/site.webmanifest (100%) rename frontend/{app-images => public/images}/logo-256.png (100%) create mode 100644 frontend/public/images/logo-bold-horizontal-grey.svg create mode 100644 frontend/public/images/logo-no-text.svg create mode 100644 frontend/public/images/logo-text-horizontal-grey.png rename frontend/{app-images => public/images}/logo-text-vertical-grey.png (100%) create mode 100644 frontend/public/index.html delete mode 100644 frontend/scss/custom.scss delete mode 100644 frontend/scss/fonts.scss delete mode 100644 frontend/scss/selectize.scss delete mode 100644 frontend/scss/styles.scss delete mode 100644 frontend/scss/tabler-extra.scss create mode 100644 frontend/src/App.test.tsx create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/Router.tsx create mode 100644 frontend/src/api/npm/base.ts create mode 100644 frontend/src/api/npm/createCertificateAuthority.ts create mode 100644 frontend/src/api/npm/createDNSProvider.ts create mode 100644 frontend/src/api/npm/createUser.ts create mode 100644 frontend/src/api/npm/getCertificateAuthorities.ts create mode 100644 frontend/src/api/npm/getCertificateAuthority.ts create mode 100644 frontend/src/api/npm/getCertificates.ts create mode 100644 frontend/src/api/npm/getDNSProvider.ts create mode 100644 frontend/src/api/npm/getDNSProviders.ts create mode 100644 frontend/src/api/npm/getDNSProvidersAcmesh.ts create mode 100644 frontend/src/api/npm/getHealth.ts create mode 100644 frontend/src/api/npm/getHostTemplates.ts create mode 100644 frontend/src/api/npm/getHosts.ts create mode 100644 frontend/src/api/npm/getSettings.ts create mode 100644 frontend/src/api/npm/getToken.ts create mode 100644 frontend/src/api/npm/getUser.ts create mode 100644 frontend/src/api/npm/getUsers.ts create mode 100644 frontend/src/api/npm/helpers.ts create mode 100644 frontend/src/api/npm/index.ts create mode 100644 frontend/src/api/npm/models.ts create mode 100644 frontend/src/api/npm/refreshToken.ts create mode 100644 frontend/src/api/npm/responseTypes.ts create mode 100644 frontend/src/api/npm/setAuth.ts create mode 100644 frontend/src/api/npm/setCertificateAuthority.ts create mode 100644 frontend/src/api/npm/setDNSProvider.ts create mode 100644 frontend/src/api/npm/setUser.ts create mode 100644 frontend/src/components/EmptyList.tsx create mode 100644 frontend/src/components/Flag/Flag.tsx create mode 100644 frontend/src/components/Flag/index.ts create mode 100644 frontend/src/components/Footer.tsx create mode 100644 frontend/src/components/HelpDrawer/HelpDrawer.tsx create mode 100644 frontend/src/components/HelpDrawer/index.ts create mode 100644 frontend/src/components/Loader/Loader.tsx create mode 100644 frontend/src/components/Loader/index.ts create mode 100644 frontend/src/components/Loading.tsx create mode 100644 frontend/src/components/LocalePicker.tsx create mode 100644 frontend/src/components/Navigation/Navigation.tsx create mode 100644 frontend/src/components/Navigation/NavigationHeader.tsx create mode 100644 frontend/src/components/Navigation/NavigationMenu.tsx create mode 100644 frontend/src/components/Navigation/index.ts create mode 100644 frontend/src/components/Permissions/AdminPermissionSelector.tsx create mode 100644 frontend/src/components/Permissions/PermissionSelector.tsx create mode 100644 frontend/src/components/Permissions/index.ts create mode 100644 frontend/src/components/PrettyButton.tsx create mode 100644 frontend/src/components/SiteWrapper.tsx create mode 100644 frontend/src/components/SpinnerPage.tsx create mode 100644 frontend/src/components/Table/Formatters.tsx create mode 100644 frontend/src/components/Table/RowActionsMenu.tsx create mode 100644 frontend/src/components/Table/TableHelpers.ts create mode 100644 frontend/src/components/Table/TableLayout.tsx create mode 100644 frontend/src/components/Table/TextFilter.tsx create mode 100644 frontend/src/components/Table/index.ts create mode 100644 frontend/src/components/Table/react-table-config.d.ts create mode 100644 frontend/src/components/ThemeSwitcher.tsx create mode 100644 frontend/src/components/Unhealthy.tsx create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/src/context/AuthContext.tsx create mode 100644 frontend/src/context/LocaleContext.tsx create mode 100644 frontend/src/context/index.ts create mode 100644 frontend/src/declarations.d.ts create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff2 create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff2 create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff2 create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff create mode 100644 frontend/src/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff2 create mode 100644 frontend/src/hooks/index.ts create mode 100644 frontend/src/hooks/useCertificateAuthorities.ts create mode 100644 frontend/src/hooks/useCertificateAuthority.ts create mode 100644 frontend/src/hooks/useCertificates.ts create mode 100644 frontend/src/hooks/useDNSProvider.ts create mode 100644 frontend/src/hooks/useDNSProviders.ts create mode 100644 frontend/src/hooks/useDNSProvidersAcmesh.ts create mode 100644 frontend/src/hooks/useHealth.ts create mode 100644 frontend/src/hooks/useHostTemplates.ts create mode 100644 frontend/src/hooks/useHosts.ts create mode 100644 frontend/src/hooks/useSettings.ts create mode 100644 frontend/src/hooks/useUser.ts create mode 100644 frontend/src/hooks/useUsers.ts create mode 100644 frontend/src/img/logo-256.png create mode 100644 frontend/src/img/logo-text-vertical-grey.png create mode 100644 frontend/src/index.scss create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/locale/IntlProvider.tsx create mode 100644 frontend/src/locale/index.ts create mode 100644 frontend/src/locale/src/HelpDoc/de/CertificateAuthorities.md create mode 100644 frontend/src/locale/src/HelpDoc/de/index.ts create mode 100644 frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md create mode 100644 frontend/src/locale/src/HelpDoc/en/index.ts create mode 100644 frontend/src/locale/src/HelpDoc/fa/CertificateAuthorities.md create mode 100644 frontend/src/locale/src/HelpDoc/fa/index.ts create mode 100644 frontend/src/locale/src/HelpDoc/index.ts create mode 100644 frontend/src/locale/src/de.json create mode 100644 frontend/src/locale/src/en.json create mode 100644 frontend/src/locale/src/fa.json create mode 100644 frontend/src/locale/src/lang-list.json create mode 100644 frontend/src/modals/CertificateAuthorityCreateModal.tsx create mode 100644 frontend/src/modals/CertificateAuthorityEditModal.tsx create mode 100644 frontend/src/modals/ChangePasswordModal.tsx create mode 100644 frontend/src/modals/DNSProviderCreateModal.tsx create mode 100644 frontend/src/modals/ProfileModal.tsx create mode 100644 frontend/src/modals/SetPasswordModal.tsx create mode 100644 frontend/src/modals/UserCreateModal.tsx create mode 100644 frontend/src/modals/UserEditModal.tsx create mode 100644 frontend/src/modals/index.ts create mode 100644 frontend/src/modules/Acmesh.ts create mode 100644 frontend/src/modules/AuthStore.ts create mode 100644 frontend/src/modules/Validations.tsx create mode 100644 frontend/src/pages/AccessLists/index.tsx create mode 100644 frontend/src/pages/AuditLog/index.tsx create mode 100644 frontend/src/pages/CertificateAuthorities/Table.tsx create mode 100644 frontend/src/pages/CertificateAuthorities/TableWrapper.tsx create mode 100644 frontend/src/pages/CertificateAuthorities/index.tsx create mode 100644 frontend/src/pages/Certificates/CertificatesTable.tsx create mode 100644 frontend/src/pages/Certificates/index.tsx create mode 100644 frontend/src/pages/DNSProviders/Table.tsx create mode 100644 frontend/src/pages/DNSProviders/TableWrapper.tsx create mode 100644 frontend/src/pages/DNSProviders/index.tsx create mode 100644 frontend/src/pages/Dashboard/index.tsx create mode 100644 frontend/src/pages/HostTemplates/HostTemplatesTable.tsx create mode 100644 frontend/src/pages/HostTemplates/index.tsx create mode 100644 frontend/src/pages/Hosts/HostsTable.tsx create mode 100644 frontend/src/pages/Hosts/index.tsx create mode 100644 frontend/src/pages/Login/index.tsx create mode 100644 frontend/src/pages/Settings/SettingsTable.tsx create mode 100644 frontend/src/pages/Settings/index.tsx create mode 100644 frontend/src/pages/Setup/index.tsx create mode 100644 frontend/src/pages/Users/Table.tsx create mode 100644 frontend/src/pages/Users/TableWrapper.tsx create mode 100644 frontend/src/pages/Users/index.tsx create mode 100644 frontend/src/react-app-env.d.ts create mode 100644 frontend/src/styles/fonts.scss create mode 100644 frontend/src/theme/customTheme.ts create mode 100644 frontend/tsconfig.json delete mode 100644 frontend/webpack.config.js delete mode 100644 global/certbot-dns-plugins.js create mode 100755 scripts/ci/build-backend create mode 100755 scripts/ci/build-cleanup create mode 100755 scripts/ci/build-frontend create mode 100755 scripts/ci/fulltest-cypress create mode 100755 scripts/ci/test-backend delete mode 100755 scripts/frontend-build create mode 100755 scripts/frontend-lint create mode 100755 scripts/go-multiarch-wrapper create mode 100755 scripts/sqlite delete mode 100644 test/.dockerignore create mode 100644 test/cypress/integration/api/Certificates.spec.js create mode 100644 test/cypress/integration/api/FullCertProvision.spec.js create mode 100644 test/cypress/integration/api/Settings.spec.js create mode 100644 test/cypress/integration/api/SetupPhase.spec.js create mode 100644 test/cypress/integration/api/SwaggerSchema.spec.js create mode 100644 test/cypress/integration/ui/SetupLogin.spec.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2ff6c7e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# Ignore everything +* + +# Only allow the following for docker build: +!backend/ +!docker/ +!scripts/ +!test/ diff --git a/.gitignore b/.gitignore index 08462849..7c00febf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,25 @@ +git.idea +.env .DS_Store -.idea ._* +*.code-workspace +vendor +bin/* +backend/config.json +backend/embed/assets +test/node_modules +*/node_modules +docs/.vuepress/dist +frontend/build +frontend/yarn-error.log +frontend/.npmrc +frontend/src/locale/lang +test/cypress/fixtures/example.json .vscode -certbot-help.txt +docker-build +data +dist +backend/embed/acme.sh +docker/dev/resolv.conf +docker/dev/dnsrouter-config.json.tmp + diff --git a/.version b/.version index b0e185b7..5efd7ac5 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.9.18 +3.0.0a diff --git a/DEV-README.md b/DEV-README.md new file mode 100644 index 00000000..df593536 --- /dev/null +++ b/DEV-README.md @@ -0,0 +1,93 @@ +# Development + +```bash +git clone nginxproxymanager +cd nginxproxymanager +./scripts/start-dev +# wait a minute or 2 for the package to build after container start +curl http://127.0.0.1:3081/api/ +``` + +## Using Local Test Certificate Authorities + +It's handy to use these instead of hitting production or staging acme servers +when testing lots of stuff. + +Firstly create your first user using the api: + +```bash +curl --request POST \ + --url http://127.0.0.1:3081/api/users \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "Bobby Tables", + "nickname": "Bobby", + "email": "you@example.com", + "roles": ["admin"], + "is_disabled": false, + "auth": { + "type": "password", + "secret": "changeme" + } +}' +``` + +Then login in with those credentials to get your JWT token and set +that as an environment variable: + +```bash +NPM_TOKEN=$(curl --request POST \ + --url http://127.0.0.1:3081/api/tokens \ + --header 'Content-Type: application/json' \ + --data '{ + "type": "password", + "identity": "you@example.com", + "secret": "changeme" +}' | jq -r '.result.token') +``` + +Then choose one or both of the following CA's to set up. + +### SmallStep Acme CA + +[StepCA](https://github.com/smallstep/certificates) is SmallSteps's test CA server. + +- ✅ HTTP Validation +- ✅ DNS Validation +\ +Create a Certificate Authority that points to the Step CA: + +```bash +curl --request POST \ + --url http://127.0.0.1:3081/api/certificate-authorities \ + --header "Authorization: Bearer ${NPM_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "Step CA", + "acmesh_server": "https://ca.internal/acme/acme/directory", + "ca_bundle": "/etc/ssl/certs/NginxProxyManager.crt", + "max_domains": 2 +}' +``` + +### Pebble Test Acme CA + +[Pebble](https://github.com/letsencrypt/pebble) is Let's Encrypt's own test CA server. + +- ✅ HTTP Validation +- ❌ DNS Validation + +Create a Certificate Authority that points to the Pebble CA: + +```bash +curl --request POST \ + --url http://127.0.0.1:3081/api/certificate-authorities \ + --header "Authorization: Bearer ${NPM_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "Pebble CA", + "acmesh_server": "https://pebble/dir", + "ca_bundle": "/etc/ssl/certs/pebble.minica.pem", + "max_domains": 2 +}' +``` diff --git a/Jenkinsfile b/Jenkinsfile index 1b744692..0f335d2b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,14 +8,18 @@ pipeline { ansiColor('xterm') } environment { - IMAGE = "nginx-proxy-manager" + DOCKER_ORG = 'jc21' + IMAGE = 'nginx-proxy-manager' BUILD_VERSION = getVersion() - MAJOR_VERSION = "2" + BUILD_COMMIT = getCommit() + MAJOR_VERSION = '3' BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_FILE = 'docker/docker-compose.ci.yml' COMPOSE_INTERACTIVE_NO_CLI = 1 BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" + DOCS_BUCKET = 'jc21-npm-site-next' // TODO: change to prod when official + DOCS_CDN = 'E2Z0128EHS0Q23' // TODO: same } stages { stage('Environment') { @@ -26,7 +30,9 @@ pipeline { } steps { script { - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" + env.BUILDX_PUSH_TAGS = "-t docker.io/${DOCKER_ORG}/${IMAGE}:${BUILD_VERSION} -t docker.io/${DOCKER_ORG}/${IMAGE}:${MAJOR_VERSION} -t docker.io/${DOCKER_ORG}/${IMAGE}:latest" + echo 'Building on Master is disabled!' + sh 'exit 1' } } } @@ -39,100 +45,76 @@ pipeline { steps { script { // Defaults to the Branch name, which is applies to all branches AND pr's - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" + env.BUILDX_PUSH_TAGS = "-t docker.io/${DOCKER_ORG}/${IMAGE}:v3-${BRANCH_LOWER}" } } } - stage('Versions') { - steps { - sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json' - sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"' - sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json' - sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"' - sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' - } - } } } stage('Frontend') { steps { - sh './scripts/frontend-build' + sh './scripts/ci/build-frontend' } + /* + post { + always { + junit 'frontend/eslint.xml' + junit 'frontend/junit.xml' + } + } + */ } stage('Backend') { steps { - echo 'Checking Syntax ...' - sh 'docker pull nginxproxymanager/nginx-full:certbot-node' - // See: https://github.com/yarnpkg/yarn/issues/3254 - sh '''docker run --rm \\ - -v "$(pwd)/backend:/app" \\ - -v "$(pwd)/global:/app/global" \\ - -w /app \\ - nginxproxymanager/nginx-full:certbot-node \\ - sh -c "yarn install && yarn eslint . && rm -rf node_modules" - ''' - - echo 'Docker Build ...' - sh '''docker build --pull --no-cache --squash --compress \\ - -t "${IMAGE}:ci-${BUILD_NUMBER}" \\ - -f docker/Dockerfile \\ - --build-arg TARGETPLATFORM=linux/amd64 \\ - --build-arg BUILDPLATFORM=linux/amd64 \\ - --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ - --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ - --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ - . - ''' - } - } - stage('Integration Tests Sqlite') { - steps { - // Bring up a stack - sh 'docker-compose up -d fullstack-sqlite' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-sqlite' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/' + withCredentials([string(credentialsId: 'npm-sentry-dsn', variable: 'SENTRY_DSN')]) { + withCredentials([usernamePassword(credentialsId: 'oss-index-token', passwordVariable: 'NANCY_TOKEN', usernameVariable: 'NANCY_USER')]) { + sh './scripts/ci/test-backend' + } + sh './scripts/ci/build-backend' + sh '''docker build --pull --no-cache \\ + -t "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" \\ + -f docker/Dockerfile \\ + --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ + --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ + --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ + . + ''' + } } post { - always { - // Dumps to analyze later - sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' - // Cypress videos and screenshot artifacts - dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' - } - junit 'test/results/junit/*' + success { + archiveArtifacts allowEmptyArchive: false, artifacts: 'bin/*' } } } - stage('Integration Tests Mysql') { + stage('Test') { + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } steps { - // Bring up a stack - sh 'docker-compose up -d fullstack-mysql' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-mysql' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/' + // Docker image check + /* + sh '''docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(pwd)/docker:/app" \ + -e CI=true \ + wagoodman/dive:latest --ci-config /app/.dive-ci \ + "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" + ''' + */ + sh './scripts/ci/fulltest-cypress' } post { always { // Dumps to analyze later sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' - // Cypress videos and screenshot artifacts - dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' - } + sh 'docker-compose logs fullstack > debug/docker_fullstack.log' + sh 'docker-compose logs stepca > debug/docker_stepca.log' + sh 'docker-compose logs pdns > debug/docker_pdns.log' + sh 'docker-compose logs pdns-db > debug/docker_pdns-db.log' + sh 'docker-compose logs dnsrouter > debug/docker_dnsrouter.log' junit 'test/results/junit/*' } } @@ -149,11 +131,14 @@ pipeline { sh 'yarn build' } + // API Docs: + sh 'docker-compose exec -T fullstack curl -s --output /temp-docs/api-schema.json "http://fullstack:81/api/schema"' + sh 'mkdir -p "docs/.vuepress/dist/api"' + sh 'mv docs/api-schema.json docs/.vuepress/dist/api/' + dir(path: 'docs/.vuepress/dist') { sh 'tar -czf ../../docs.tgz *' } - - archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) } } stage('MultiArch Build') { @@ -163,11 +148,12 @@ pipeline { } } steps { - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - // Docker Login - sh "docker login -u '${duser}' -p '${dpass}'" - // Buildx with push from cache - sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" + withCredentials([string(credentialsId: 'npm-sentry-dsn', variable: 'SENTRY_DSN')]) { + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh 'docker login -u "${duser}" -p "${dpass}"' + sh './scripts/buildx --push ${BUILDX_PUSH_TAGS}' + // sh './scripts/buildx -o type=local,dest=docker-build' + } } } } @@ -184,7 +170,7 @@ pipeline { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh """docker run --rm \\ --name \${COMPOSE_PROJECT_NAME}-docs-upload \\ - -e S3_BUCKET=jc21-npm-site \\ + -e S3_BUCKET=$DOCS_BUCKET \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ -v \$(pwd):/app \\ @@ -198,7 +184,7 @@ pipeline { -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ jc21/ci-tools \\ - aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*' + aws cloudfront create-invalidation --distribution-id $DOCS_CDN --paths '/*' """ } } @@ -214,7 +200,7 @@ pipeline { } steps { script { - def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") + def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/${DOCKER_ORG}/${IMAGE}) as `${DOCKER_ORG}/${IMAGE}:v3-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") } } } @@ -222,19 +208,26 @@ pipeline { post { always { sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' - sh 'echo Reverting ownership' - sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' + sh './scripts/ci/build-cleanup' + echo 'Reverting ownership' + sh 'docker run --rm -v $(pwd):/data jc21/gotools:latest chown -R "$(id -u):$(id -g)" /data' } success { juxtapose event: 'success' sh 'figlet "SUCCESS"' } failure { + dir(path: 'test') { + archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml' + } archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) juxtapose event: 'failure' sh 'figlet "FAILURE"' } unstable { + dir(path: 'test') { + archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml' + } archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) juxtapose event: 'unstable' sh 'figlet "UNSTABLE"' diff --git a/README.md b/README.md index a97d3ba8..775bc244 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@



- + @@ -52,7 +52,8 @@ I won't go in to too much detail here but here are the basics for someone new to 3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) 4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services -## Quick Setup + +## Quickest Setup 1. Install Docker and Docker-Compose @@ -65,7 +66,7 @@ I won't go in to too much detail here but here are the basics for someone new to version: '3' services: app: - image: 'jc21/nginx-proxy-manager:latest' + image: 'jc21/nginx-proxy-manager:v3-develop' restart: unless-stopped ports: - '80:80' @@ -73,7 +74,6 @@ services: - '443:443' volumes: - ./data:/data - - ./letsencrypt:/etc/letsencrypt ``` 3. Bring up your stack by running @@ -97,436 +97,6 @@ Password: changeme Immediately after logging in with this default user you will be asked to modify your details and change your password. +## Become a Contributor -## Contributors - -Special thanks to the following contributors: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
chaptergy -
-
- - -
Kyle Klaus -
-
- - -
ƬHE ЯAW -
-
- - -
Spencer -
-
- - -
Xantios Krugor -
-
- - -
David Panesso -
-
- - -
IronTooch -
-
- - -
Damiano -
-
- - -
Russ -
-
- - -
Marcelo Castagna -
-
- - -
Steven Harris -
-
- - -
Jocelyn Le Sage -
-
- - -
Carl Mercier -
-
- - -
Paul Mansfield -
-
- - -
OhHeyAlan -
-
- - -
Carl Sutton -
-
- - -
Gergő Törcsvári -
-
- - -
vrenjith -
-
- - -
David Rivera -
-
- - -
Jaap-Jan de Wit -
-
- - -
James Morgan -
-
- - -
Sebastian Valle -
-
- - -
Philip Mooney -
-
- - -
WaterCalm -
-
- - -
lebrou34 -
-
- - -
Mário Franco -
-
- - -
Kyle Harding -
-
- - -
Alex Graber -
-
- - -
MooBaloo -
-
- - -
Shuro -
-
- - -
Loris Bergeron -
-
- - -
hepelayo -
-
- - -
Jonas Leder -
-
- - -
Bastian Stegmann -
-
- - -
Stealthii -
-
- - -
THEGamingninja -
-
- - -
Italo Borssatto -
-
- - -
Gurjinder Singh -
-
- - -
David Dosoudil -
-
- - -
ijaron -
-
- - -
Niels Bouma -
-
- - -
Orko Garai -
-
- - -
Filippo Baruffaldi -
-
- - -
Bikramjeet Singh -
-
- - -
Razvan Stoica -
-
- - -
RBXII3 -
-
- - -
demize -
-
- - -
PUP-Loki -
-
- - -
Daniel Sörlöv -
-
- - -
Theyooo -
-
- - -
Justin Peacock -
-
- - -
Chris Tracy -
-
- - -
Fuechslein -
-
- - -
Amir Zarrinkafsh -
-
- - -
gabbe -
-
- - -
bmbvenom -
-
- - -
Florian Meinicke -
-
- - -
Rahul Somasundaram -
-
- - -
Björn Heinrichs -
-
- - -
Josh Byrnes -
-
- - -
bergi9 -
-
- - -
luoweihua7 -
-
- - -
Tobias Kneidl -
-
- - -
Pius Walter -
-
- - -
Troy Kelly -
-
- - -
Ivan Kristianto -
-
- - -
Omer Cohen -
-
- - +A guide to setting up your own development environment [is found here](DEV-README.md). diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 00000000..8b96428f --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json deleted file mode 100644 index 6d6172a4..00000000 --- a/backend/.eslintrc.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "extends": [ - "eslint:recommended" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "align-assignments" - ], - "rules": { - "arrow-parens": [ - "error", - "always" - ], - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "key-spacing": [ - "error", - { - "align": "value" - } - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "func-call-spacing": [ - "error", - "never" - ], - "keyword-spacing": [ - "error", - { - "before": true - } - ], - "no-irregular-whitespace": "error", - "no-unused-expressions": 0, - "align-assignments/align-assignments": [ - 2, - { - "requiresOnly": false - } - ] - } -} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 149080b9..00000000 --- a/backend/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -config/development.json -data/* -yarn-error.log -tmp -certbot.log -node_modules -core.* - diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 00000000..f61dc69d --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,92 @@ +linters: + enable: + # Prevents against memory leaks in production caused by not closing file handle + - bodyclose + # Detects unused declarations in a go package + - deadcode + # Detects cloned code. DRY is good programming practice. Can cause issues with testing code where + # simplicity is preferred over duplication. Disabled for test code. + #- dupl + # Detects unchecked errors in go programs. These unchecked errors can be critical bugs in some cases. + - errcheck + # Simplifies go code. + - gosimple + # Reports suspicious constructs, maintained by goteam. e.g. Printf unused params not caught + # at compile time. + - govet + # Detect security issues with gocode. Use of secrets in code or obsolete security algorithms. + # It's imaged heuristic methods are used in finding problems. If issues with rules are found + # particular rules can be disabled as required. + # Could possibility cause issues with testing. Disabled for test code. + - gosec + # Detect repeated strings that could be replaced by a constant + - goconst + # Misc linters missing from other projects. Grouped into 3 categories diagnostics, style + # and performance + - gocritic + # Limits code cyclomatic complexity + - gocyclo + # Detects if code needs to be gofmt'd + - gofmt + # Detects unused go package imports + - goimports + # Detcts style mistakes not correctness. Golint is meant to carry out the + # stylistic conventions put forth in Effective Go and CodeReviewComments. + # golint has false positives and false negatives and can be tweaked. + - golint + # Detects ineffectual assignments in code + - ineffassign + # Detect commonly misspelled english words in comments + - misspell + # Detect naked returns on non-trivial functions, and conform with Go CodeReviewComments + - nakedret + # Detect slice allocations that can be preallocated + - prealloc + # Misc collection of static analysis tools + - staticcheck + # Detects unused struct fields + - structcheck + # Parses and typechecks the code like the go compiler + - typecheck + # Detects unused constants, variables, functions and types + - unused + # Detects unused global variables and constants + - varcheck + # Remove unnecessary type conversions + - unconvert + # Remove unnecessary(unused) function parameters + - unparam +linters-settings: + goconst: + # minimal length of string constant + # default: 3 + min-len: 2 + # minimum number of occurrences of string constant + # default: 3 + min-occurences: 2 + misspell: + locale: UK + ignore-words: + - color +issues: + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + # We have chosen an arbitrary value that works based on practical usage. + max-same: 20 + # See cmdline flag documentation for more info about default excludes --exclude-use-default + # Nothing is excluded by default + exclude-use-default: false + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. # TODO: Add examples why this is good + + - path: _test\.go + linters: + # Tests should be simple? Add example why this is good? + - gocyclo + # Error checking adds verbosity and complexity for minimal value + - errcheck + # Table test encourage duplication in defining the table tests. + - dupl + # Hard coded example tokens, SQL injection and other bad practices may + # want to be tested + - gosec diff --git a/backend/.nancy-ignore b/backend/.nancy-ignore new file mode 100644 index 00000000..5736e87a --- /dev/null +++ b/backend/.nancy-ignore @@ -0,0 +1,22 @@ +# If you need to ignore any of nancy's warnings add them +# here with a reference to the package/version that +# triggers them and rational for ignoring it. + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# etcd before versions 3.3.23 and 3.4.10 does not perform any password length validation +CVE-2020-15115 + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# In ectd before versions 3.4.10 and 3.3.23, gateway TLS authentication is only applied to endpoints detected in DNS SRV records +CVE-2020-15136 + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# In etcd before versions 3.3.23 and 3.4.10, the etcd gateway is a simple TCP proxy to allow for basic service discovery and access +CVE-2020-15114 + +# pkg:golang/github.com/gorilla/websocket@1.4.0 +# Integer Overflow or Wraparound +CWE-190 + +# jwt-go before 4.0.0-preview1 allows attackers to bypass intended access restrict... +CVE-2020-26160 diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json deleted file mode 100644 index 4e540ab3..00000000 --- a/backend/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "editor.insertSpaces": false, - "editor.formatOnSave": true, - "files.trimTrailingWhitespace": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } -} \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..912f0069 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,6 @@ +# Backend + +## Guides and materials + +- [Nginx Proxy Protocol](https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/) +- diff --git a/backend/Taskfile.yml b/backend/Taskfile.yml new file mode 100644 index 00000000..81a0bcd0 --- /dev/null +++ b/backend/Taskfile.yml @@ -0,0 +1,69 @@ +version: "2" + +tasks: + default: + cmds: + - task: run + + run: + desc: Build and run + sources: + - internal/**/*.go + - cmd/**/*.go + - ../frontend/src/locale/src/*.json + cmds: + - task: locale + - task: build + - cmd: echo -e "==> Running..." + silent: true + - cmd: ../dist/bin/server + ignore_error: true + silent: true + env: + LOG_LEVEL: debug + + build: + desc: Build the server + cmds: + - cmd: echo -e "==> Building..." + silent: true + - cmd: rm -f dist/bin/* + silent: true + - cmd: go build -ldflags="-X main.commit={{.GIT_COMMIT}} -X main.version={{.VERSION}}" -o ../dist/bin/server ./cmd/server/main.go + silent: true + - task: lint + vars: + GIT_COMMIT: + sh: git log -n 1 --format=%h + VERSION: + sh: cat ../.version + env: + GO111MODULE: on + CGO_ENABLED: 1 + + lint: + desc: Linting + cmds: + - cmd: echo -e "==> Linting..." + silent: true + - cmd: bash scripts/lint.sh + silent: true + + test: + desc: Testing + cmds: + - cmd: echo -e "==> Testing..." + silent: true + - cmd: bash scripts/test.sh + silent: true + + locale: + desc: Locale + dir: /app/frontend + cmds: + - cmd: yarn locale-compile + silent: true + ignore_error: true + - cmd: chown -R "$PUID:$PGID" src/locale/lang + silent: true + ignore_error: true diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index ca6d6fba..00000000 --- a/backend/app.js +++ /dev/null @@ -1,89 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const fileUpload = require('express-fileupload'); -const compression = require('compression'); -const log = require('./logger').express; - -/** - * 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 (process.env.NODE_ENV !== 'production') { - app.set('json spaces', 2); -} - -// CORS for everything -app.use(require('./lib/express/cors')); - -// General security/cache related headers + server header -app.use(function (req, 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(require('./lib/express/jwt')()); -app.use('/', require('./routes/api/main')); - -// production error handler -// no stacktraces leaked to user -// eslint-disable-next-line -app.use(function (err, req, res, next) { - - let payload = { - error: { - code: err.status, - message: err.public ? err.message : 'Internal Error' - } - }; - - if (process.env.NODE_ENV === 'development' || (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) { - if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { - log.debug(err.stack); - } else if (typeof err.public == 'undefined' || !err.public) { - log.warn(err.message); - } - } - - res - .status(err.status || 500) - .send(payload); -}); - -module.exports = app; diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 00000000..23e9bdc4 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "npm/internal/api" + "npm/internal/config" + "npm/internal/database" + "npm/internal/entity/setting" + "npm/internal/logger" + "npm/internal/state" + "npm/internal/worker" +) + +var commit string +var version string +var sentryDSN string + +func main() { + config.InitArgs(&version, &commit) + config.Init(&version, &commit, &sentryDSN) + appstate := state.NewState() + + database.Migrate(func() { + setting.ApplySettings() + database.CheckSetup() + go worker.StartCertificateWorker(appstate) + + api.StartServer() + irqchan := make(chan os.Signal, 1) + signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM) + + for irq := range irqchan { + if irq == syscall.SIGINT || irq == syscall.SIGTERM { + logger.Info("Got ", irq, " shutting server down ...") + // Close db + err := database.GetInstance().Close() + if err != nil { + logger.Error("DatabaseCloseError", err) + } + break + } + } + }) +} diff --git a/backend/config/README.md b/backend/config/README.md deleted file mode 100644 index 26268a11..00000000 --- a/backend/config/README.md +++ /dev/null @@ -1,2 +0,0 @@ -These files are use in development and are not deployed as part of the final product. - \ No newline at end of file diff --git a/backend/config/default.json b/backend/config/default.json deleted file mode 100644 index 64ab577c..00000000 --- a/backend/config/default.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "database": { - "engine": "mysql", - "host": "db", - "name": "npm", - "user": "npm", - "password": "npm", - "port": 3306 - } -} diff --git a/backend/config/sqlite-test-db.json b/backend/config/sqlite-test-db.json deleted file mode 100644 index ad548865..00000000 --- a/backend/config/sqlite-test-db.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "database": { - "engine": "knex-native", - "knex": { - "client": "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" - } - } - } -} diff --git a/backend/db.js b/backend/db.js deleted file mode 100644 index ce5338f0..00000000 --- a/backend/db.js +++ /dev/null @@ -1,33 +0,0 @@ -const config = require('config'); - -if (!config.has('database')) { - throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md'); -} - -function generateDbConfig() { - if (config.database.engine === 'knex-native') { - return config.database.knex; - } else - return { - client: config.database.engine, - connection: { - host: config.database.host, - user: config.database.user, - password: config.database.password, - database: config.database.name, - port: config.database.port - }, - migrations: { - tableName: 'migrations' - } - }; -} - - -let data = generateDbConfig(); - -if (typeof config.database.version !== 'undefined') { - data.version = config.database.version; -} - -module.exports = require('knex')(data); diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json deleted file mode 100644 index 06c02564..00000000 --- a/backend/doc/api.swagger.json +++ /dev/null @@ -1,1254 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Nginx Proxy Manager API", - "version": "2.x.x" - }, - "servers": [ - { - "url": "http://127.0.0.1:81/api" - } - ], - "paths": { - "/": { - "get": { - "operationId": "health", - "summary": "Returns the API health status", - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "status": "OK", - "version": { - "major": 2, - "minor": 1, - "revision": 0 - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/HealthObject" - } - } - } - } - } - } - }, - "/schema": { - "get": { - "operationId": "schema", - "responses": { - "200": { - "description": "200 response" - } - }, - "summary": "Returns this swagger API schema" - } - }, - "/tokens": { - "get": { - "operationId": "refreshToken", - "summary": "Refresh your access token", - "tags": [ - "Tokens" - ], - "security": [ - { - "BearerAuth": [ - "tokens" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - } - } - } - }, - "post": { - "operationId": "requestToken", - "parameters": [ - { - "description": "Credentials Payload", - "in": "body", - "name": "credentials", - "required": true, - "schema": { - "additionalProperties": false, - "properties": { - "identity": { - "minLength": 1, - "type": "string" - }, - "scope": { - "minLength": 1, - "type": "string", - "enum": [ - "user" - ] - }, - "secret": { - "minLength": 1, - "type": "string" - } - }, - "required": [ - "identity", - "secret" - ], - "type": "object" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "result": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - }, - "description": "200 response" - } - }, - "summary": "Request a new access token from credentials", - "tags": [ - "Tokens" - ] - } - }, - "/settings": { - "get": { - "operationId": "getSettings", - "summary": "Get all settings", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/SettingsList" - } - } - } - } - } - } - }, - "/settings/{settingID}": { - "get": { - "operationId": "getSetting", - "summary": "Get a setting", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } - }, - "put": { - "operationId": "updateSetting", - "summary": "Update a setting", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - }, - { - "in": "body", - "name": "setting", - "description": "Setting Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } - } - }, - "/users": { - "get": { - "operationId": "getUsers", - "summary": "Get all users", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "query", - "name": "expand", - "description": "Expansions", - "schema": { - "type": "string", - "enum": [ - "permissions" - ] - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - ] - }, - "withPermissions": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/UsersList" - } - } - } - } - } - }, - "post": { - "operationId": "createUser", - "summary": "Create a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "201": { - "description": "201 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - } - }, - "/users/{userID}": { - "get": { - "operationId": "getUser", - "summary": "Get a user", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 1 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - }, - "put": { - "operationId": "updateUser", - "summary": "Update a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - }, - "delete": { - "operationId": "deleteUser", - "summary": "Delete a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/auth": { - "put": { - "operationId": "updateUserAuth", - "summary": "Update a User's Authentication", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/AuthObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/permissions": { - "put": { - "operationId": "updateUserPermissions", - "summary": "Update a User's Permissions", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "Permissions Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/PermissionsObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/login": { - "put": { - "operationId": "loginAsUser", - "summary": "Login as this user", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg", - "expires": "2020-01-31T10:56:23.239Z", - "user": { - "id": 1, - "created_on": "2020-01-30T10:43:44.000Z", - "modified_on": "2020-01-30T10:43:44.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", - "roles": [ - "admin" - ] - } - } - } - }, - "schema": { - "type": "object", - "description": "Login object", - "required": [ - "expires", - "token", - "user" - ], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - }, - "user": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - } - } - } - }, - "/reports/hosts": { - "get": { - "operationId": "reportsHosts", - "summary": "Report on Host Statistics", - "tags": [ - "Reports" - ], - "security": [ - { - "BearerAuth": [ - "reports" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } - } - }, - "/audit-log": { - "get": { - "operationId": "getAuditLog", - "summary": "Get Audit Log", - "tags": [ - "Audit Log" - ], - "security": [ - { - "BearerAuth": [ - "audit-log" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } - } - } - }, - "components": { - "securitySchemes": { - "BearerAuth": { - "type": "http", - "scheme": "bearer" - } - }, - "schemas": { - "HealthObject": { - "type": "object", - "description": "Health object", - "additionalProperties": false, - "required": [ - "status", - "version" - ], - "properties": { - "status": { - "type": "string", - "description": "Healthy", - "example": "OK" - }, - "version": { - "type": "object", - "description": "The version object", - "example": { - "major": 2, - "minor": 0, - "revision": 0 - }, - "additionalProperties": false, - "required": [ - "major", - "minor", - "revision" - ], - "properties": { - "major": { - "type": "integer", - "minimum": 0 - }, - "minor": { - "type": "integer", - "minimum": 0 - }, - "revision": { - "type": "integer", - "minimum": 0 - } - } - } - } - }, - "TokenObject": { - "type": "object", - "description": "Token object", - "required": [ - "expires", - "token" - ], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - } - } - }, - "SettingObject": { - "type": "object", - "description": "Setting object", - "required": [ - "id", - "name", - "description", - "value", - "meta" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "Setting ID", - "minLength": 1, - "example": "default-site" - }, - "name": { - "type": "string", - "description": "Setting Display Name", - "minLength": 1, - "example": "Default Site" - }, - "description": { - "type": "string", - "description": "Meaningful description", - "minLength": 1, - "example": "What to show when Nginx is hit with an unknown Host" - }, - "value": { - "description": "Value in almost any form", - "example": "congratulations", - "oneOf": [ - { - "type": "string", - "minLength": 1 - }, - { - "type": "integer" - }, - { - "type": "object" - }, - { - "type": "number" - }, - { - "type": "array" - } - ] - }, - "meta": { - "description": "Extra metadata", - "example": {}, - "type": "object" - } - } - }, - "SettingsList": { - "type": "array", - "description": "Setting list", - "items": { - "$ref": "#/components/schemas/SettingObject" - } - }, - "UserObject": { - "type": "object", - "description": "User object", - "required": [ - "id", - "created_on", - "modified_on", - "is_disabled", - "email", - "name", - "nickname", - "avatar", - "roles" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": "integer", - "description": "User ID", - "minimum": 1, - "example": 1 - }, - "created_on": { - "type": "string", - "description": "Created Date", - "example": "2020-01-30T09:36:08.000Z" - }, - "modified_on": { - "type": "string", - "description": "Modified Date", - "example": "2020-01-30T09:41:04.000Z" - }, - "is_disabled": { - "type": "integer", - "minimum": 0, - "maximum": 1, - "description": "Is user Disabled (0 = false, 1 = true)", - "example": 0 - }, - "email": { - "type": "string", - "description": "Email", - "minLength": 3, - "example": "jc@jc21.com" - }, - "name": { - "type": "string", - "description": "Name", - "minLength": 1, - "example": "Jamie Curnow" - }, - "nickname": { - "type": "string", - "description": "Nickname", - "example": "James" - }, - "avatar": { - "type": "string", - "description": "Gravatar URL based on email, without scheme", - "example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm" - }, - "roles": { - "description": "Roles applied", - "example": [ - "admin" - ], - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UsersList": { - "type": "array", - "description": "User list", - "items": { - "$ref": "#/components/schemas/UserObject" - } - }, - "AuthObject": { - "type": "object", - "description": "Authentication Object", - "required": [ - "type", - "secret" - ], - "properties": { - "type": { - "type": "string", - "pattern": "^password$", - "example": "password" - }, - "current": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "example": "changeme" - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 64, - "example": "mySuperN3wP@ssword!" - } - } - }, - "PermissionsObject": { - "type": "object", - "properties": { - "visibility": { - "type": "string", - "description": "Visibility Type", - "enum": [ - "all", - "user" - ] - }, - "access_lists": { - "type": "string", - "description": "Access Lists Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "dead_hosts": { - "type": "string", - "description": "404 Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "proxy_hosts": { - "type": "string", - "description": "Proxy Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "redirection_hosts": { - "type": "string", - "description": "Redirection Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "streams": { - "type": "string", - "description": "Streams Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "certificates": { - "type": "string", - "description": "Certificates Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - } - } - }, - "HostReportObject": { - "type": "object", - "properties": { - "proxy": { - "type": "integer", - "description": "Proxy Hosts Count" - }, - "redirection": { - "type": "integer", - "description": "Redirection Hosts Count" - }, - "stream": { - "type": "integer", - "description": "Streams Count" - }, - "dead": { - "type": "integer", - "description": "404 Hosts Count" - } - } - } - } - } -} \ No newline at end of file diff --git a/backend/embed/api_docs/api.swagger.json b/backend/embed/api_docs/api.swagger.json new file mode 100644 index 00000000..c22f9450 --- /dev/null +++ b/backend/embed/api_docs/api.swagger.json @@ -0,0 +1,243 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Nginx Proxy Manager API", + "version": "{{VERSION}}" + }, + "paths": { + "/": { + "get": { + "$ref": "file://./paths/get.json" + } + }, + "/certificates": { + "get": { + "$ref": "file://./paths/certificates/get.json" + }, + "post": { + "$ref": "file://./paths/certificates/post.json" + } + }, + "/certificates/{certificateID}": { + "get": { + "$ref": "file://./paths/certificates/certificateID/get.json" + }, + "put": { + "$ref": "file://./paths/certificates/certificateID/put.json" + }, + "delete": { + "$ref": "file://./paths/certificates/certificateID/delete.json" + } + }, + "/certificates-authorities": { + "get": { + "$ref": "file://./paths/certificates-authorities/get.json" + }, + "post": { + "$ref": "file://./paths/certificates-authorities/post.json" + } + }, + "/certificates-authorities/{caID}": { + "get": { + "$ref": "file://./paths/certificates-authorities/caID/get.json" + }, + "put": { + "$ref": "file://./paths/certificates-authorities/caID/put.json" + }, + "delete": { + "$ref": "file://./paths/certificates-authorities/caID/delete.json" + } + }, + "/config": { + "get": { + "$ref": "file://./paths/config/get.json" + } + }, + "/dns-providers": { + "get": { + "$ref": "file://./paths/dns-providers/get.json" + }, + "post": { + "$ref": "file://./paths/dns-providers/post.json" + } + }, + "/dns-providers/{providerID}": { + "get": { + "$ref": "file://./paths/dns-providers/providerID/get.json" + }, + "put": { + "$ref": "file://./paths/dns-providers/providerID/put.json" + }, + "delete": { + "$ref": "file://./paths/dns-providers/providerID/delete.json" + } + }, + "/hosts": { + "get": { + "$ref": "file://./paths/hosts/get.json" + }, + "post": { + "$ref": "file://./paths/hosts/post.json" + } + }, + "/hosts/{hostID}": { + "get": { + "$ref": "file://./paths/hosts/hostID/get.json" + }, + "put": { + "$ref": "file://./paths/hosts/hostID/put.json" + }, + "delete": { + "$ref": "file://./paths/hosts/hostID/delete.json" + } + }, + "/schema": { + "get": { + "$ref": "file://./paths/schema/get.json" + } + }, + "/settings": { + "get": { + "$ref": "file://./paths/settings/get.json" + }, + "post": { + "$ref": "file://./paths/settings/post.json" + } + }, + "/settings/{name}": { + "get": { + "$ref": "file://./paths/settings/name/get.json" + }, + "put": { + "$ref": "file://./paths/settings/name/put.json" + } + }, + "/streams": { + "get": { + "$ref": "file://./paths/streams/get.json" + }, + "post": { + "$ref": "file://./paths/streams/post.json" + } + }, + "/streams/{streamID}": { + "get": { + "$ref": "file://./paths/streams/streamID/get.json" + }, + "put": { + "$ref": "file://./paths/streams/streamID/put.json" + }, + "delete": { + "$ref": "file://./paths/streams/streamID/delete.json" + } + }, + "/tokens": { + "get": { + "$ref": "file://./paths/tokens/get.json" + }, + "post": { + "$ref": "file://./paths/tokens/post.json" + } + }, + "/users": { + "get": { + "$ref": "file://./paths/users/get.json" + }, + "post": { + "$ref": "file://./paths/users/post.json" + } + }, + "/users/{userID}": { + "get": { + "$ref": "file://./paths/users/userID/get.json" + }, + "put": { + "$ref": "file://./paths/users/userID/put.json" + }, + "delete": { + "$ref": "file://./paths/users/userID/delete.json" + } + }, + "/users/{userID}/auth": { + "post": { + "$ref": "file://./paths/users/userID/auth/post.json" + } + } + }, + "components": { + "schemas": { + "CertificateAuthorityList": { + "$ref": "file://./components/CertificateAuthorityList.json" + }, + "CertificateAuthorityObject": { + "$ref": "file://./components/CertificateAuthorityObject.json" + }, + "CertificateList": { + "$ref": "file://./components/CertificateList.json" + }, + "CertificateObject": { + "$ref": "file://./components/CertificateObject.json" + }, + "ConfigObject": { + "$ref": "file://./components/ConfigObject.json" + }, + "DeletedItemResponse": { + "$ref": "file://./components/DeletedItemResponse.json" + }, + "DNSProviderList": { + "$ref": "file://./components/DNSProviderList.json" + }, + "DNSProviderObject": { + "$ref": "file://./components/DNSProviderObject.json" + }, + "ErrorObject": { + "$ref": "file://./components/ErrorObject.json" + }, + "FilterObject": { + "$ref": "file://./components/FilterObject.json" + }, + "HealthObject": { + "$ref": "file://./components/HealthObject.json" + }, + "HostList": { + "$ref": "file://./components/HostList.json" + }, + "HostObject": { + "$ref": "file://./components/HostObject.json" + }, + "HostTemplateList": { + "$ref": "file://./components/HostTemplateList.json" + }, + "HostTemplateObject": { + "$ref": "file://./components/HostTemplateObject.json" + }, + "SettingList": { + "$ref": "file://./components/SettingList.json" + }, + "SettingObject": { + "$ref": "file://./components/SettingObject.json" + }, + "SortObject": { + "$ref": "file://./components/SortObject.json" + }, + "StreamList": { + "$ref": "file://./components/StreamList.json" + }, + "StreamObject": { + "$ref": "file://./components/StreamObject.json" + }, + "TokenObject": { + "$ref": "file://./components/TokenObject.json" + }, + "UserAuthObject": { + "$ref": "file://./components/UserAuthObject.json" + }, + "UserList": { + "$ref": "file://./components/UserList.json" + }, + "UserObject": { + "$ref": "file://./components/UserObject.json" + } + } + } +} diff --git a/backend/embed/api_docs/components/CertificateAuthorityList.json b/backend/embed/api_docs/components/CertificateAuthorityList.json new file mode 100644 index 00000000..131140ef --- /dev/null +++ b/backend/embed/api_docs/components/CertificateAuthorityList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "CertificateAuthorityList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/CertificateAuthorityObject.json b/backend/embed/api_docs/components/CertificateAuthorityObject.json new file mode 100644 index 00000000..a25cad48 --- /dev/null +++ b/backend/embed/api_docs/components/CertificateAuthorityObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "CertificateAuthorityObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "name", + "acmesh_server", + "ca_bundle", + "max_domains", + "is_wildcard_supported", + "is_readonly" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "acmesh_server": { + "type": "string", + "minLength": 2, + "maxLength": 255 + }, + "ca_bundle": { + "type": "string", + "minLength": 0, + "maxLength": 255 + }, + "max_domains": { + "type": "integer", + "minimum": 1 + }, + "is_wildcard_supported": { + "type": "boolean" + }, + "is_readonly": { + "type": "boolean" + } + } +} diff --git a/backend/embed/api_docs/components/CertificateList.json b/backend/embed/api_docs/components/CertificateList.json new file mode 100644 index 00000000..8fbf2ccc --- /dev/null +++ b/backend/embed/api_docs/components/CertificateList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "CertificateList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/CertificateObject.json b/backend/embed/api_docs/components/CertificateObject.json new file mode 100644 index 00000000..789eabf6 --- /dev/null +++ b/backend/embed/api_docs/components/CertificateObject.json @@ -0,0 +1,82 @@ +{ + "type": "object", + "description": "CertificateObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "type", + "user_id", + "certificate_authority_id", + "dns_provider_id", + "name", + "is_ecc", + "status", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1, + "nullable": true + }, + "type": { + "type": "string", + "enum": ["custom", "http", "dns"] + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "certificate_authority_id": { + "type": "integer", + "minimum": 0 + }, + "certificate_authority": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + }, + "dns_provider_id": { + "type": "integer", + "minimum": 0 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + }, + "status": { + "type": "string", + "enum": ["ready", "requesting", "failed", "provided"] + }, + "is_ecc": { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "error_message": { + "type": "string" + } + } +} diff --git a/backend/embed/api_docs/components/ConfigObject.json b/backend/embed/api_docs/components/ConfigObject.json new file mode 100644 index 00000000..96c4176f --- /dev/null +++ b/backend/embed/api_docs/components/ConfigObject.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "description": "ConfigObject" +} diff --git a/backend/embed/api_docs/components/DNSProviderList.json b/backend/embed/api_docs/components/DNSProviderList.json new file mode 100644 index 00000000..edf8385c --- /dev/null +++ b/backend/embed/api_docs/components/DNSProviderList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "DNSProviderList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/DNSProviderObject.json b/backend/embed/api_docs/components/DNSProviderObject.json new file mode 100644 index 00000000..9ea739ff --- /dev/null +++ b/backend/embed/api_docs/components/DNSProviderObject.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "description": "DNSProviderObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "user_id", + "name", + "acmesh_name", + "dns_sleep", + "meta" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "acmesh_name": { + "type": "string", + "minLength": 4, + "maxLength": 50 + }, + "dns_sleep": { + "type": "integer" + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/embed/api_docs/components/DeletedItemResponse.json b/backend/embed/api_docs/components/DeletedItemResponse.json new file mode 100644 index 00000000..0e0a9a0e --- /dev/null +++ b/backend/embed/api_docs/components/DeletedItemResponse.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "description": "DeletedItemResponse", + "additionalProperties": false, + "required": ["result"], + "properties": { + "result": { + "type": "boolean", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } +} diff --git a/backend/embed/api_docs/components/ErrorObject.json b/backend/embed/api_docs/components/ErrorObject.json new file mode 100644 index 00000000..a1d77605 --- /dev/null +++ b/backend/embed/api_docs/components/ErrorObject.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "ErrorObject", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer", + "description": "Error code", + "minimum": 0 + }, + "message": { + "type": "string", + "description": "Error message" + } + } +} diff --git a/backend/embed/api_docs/components/FilterObject.json b/backend/embed/api_docs/components/FilterObject.json new file mode 100644 index 00000000..4ba75766 --- /dev/null +++ b/backend/embed/api_docs/components/FilterObject.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "description": "FilterObject", + "additionalProperties": false, + "required": ["field", "modifier", "value"], + "properties": { + "field": { + "type": "string", + "description": "Field to filter with" + }, + "modifier": { + "type": "string", + "description": "Filter modifier", + "pattern": "^(equals|not|min|max|greater|lesser|contains|starts|ends|in|notin)$" + }, + "value": { + "type": "array", + "description": "Values used for filtering", + "items": { + "type": "string" + } + } + } +} diff --git a/backend/embed/api_docs/components/HealthObject.json b/backend/embed/api_docs/components/HealthObject.json new file mode 100644 index 00000000..c58261a7 --- /dev/null +++ b/backend/embed/api_docs/components/HealthObject.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "description": "HealthObject", + "additionalProperties": false, + "required": ["version", "commit", "healthy", "setup", "error_reporting"], + "properties": { + "version": { + "type": "string", + "description": "Version", + "example": "3.0.0", + "minLength": 1 + }, + "commit": { + "type": "string", + "description": "Commit hash", + "example": "946b88f", + "minLength": 7 + }, + "healthy": { + "type": "boolean", + "description": "Healthy?", + "example": true + }, + "setup": { + "type": "boolean", + "description": "Is the application set up?", + "example": true + }, + "error_reporting": { + "type": "boolean", + "description": "Will the application send any error reporting?", + "example": true + }, + "acme.sh": { + "type": "string", + "description": "Acme.sh version", + "example": "v3.0.0", + "minLength": 1 + } + } +} diff --git a/backend/embed/api_docs/components/HostList.json b/backend/embed/api_docs/components/HostList.json new file mode 100644 index 00000000..8d9d413a --- /dev/null +++ b/backend/embed/api_docs/components/HostList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "HostList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/HostObject.json b/backend/embed/api_docs/components/HostObject.json new file mode 100644 index 00000000..4d9eb2c9 --- /dev/null +++ b/backend/embed/api_docs/components/HostObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "HostObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "provider": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + } + } +} diff --git a/backend/embed/api_docs/components/HostTemplateList.json b/backend/embed/api_docs/components/HostTemplateList.json new file mode 100644 index 00000000..a4ad36e1 --- /dev/null +++ b/backend/embed/api_docs/components/HostTemplateList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "HostTemplateList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostTemplateObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/HostTemplateObject.json b/backend/embed/api_docs/components/HostTemplateObject.json new file mode 100644 index 00000000..ac4937a8 --- /dev/null +++ b/backend/embed/api_docs/components/HostTemplateObject.json @@ -0,0 +1,44 @@ +{ + "type": "object", + "description": "HostTemplateObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "user_id", + "name", + "host_type", + "template" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "host_type": { + "type": "string", + "pattern": "^proxy|redirect|dead|stream$" + }, + "template": { + "type": "string", + "minLength": 20 + } + } +} diff --git a/backend/embed/api_docs/components/SettingList.json b/backend/embed/api_docs/components/SettingList.json new file mode 100644 index 00000000..77fd564b --- /dev/null +++ b/backend/embed/api_docs/components/SettingList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "SettingList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SettingObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/SettingObject.json b/backend/embed/api_docs/components/SettingObject.json new file mode 100644 index 00000000..60aab614 --- /dev/null +++ b/backend/embed/api_docs/components/SettingObject.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "description": "SettingObject", + "additionalProperties": false, + "required": ["id", "name", "value"], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 100 + }, + "value": { + "oneOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } +} diff --git a/backend/embed/api_docs/components/SortObject.json b/backend/embed/api_docs/components/SortObject.json new file mode 100644 index 00000000..a2810ce6 --- /dev/null +++ b/backend/embed/api_docs/components/SortObject.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "SortObject", + "additionalProperties": false, + "required": ["field", "direction"], + "properties": { + "field": { + "type": "string", + "description": "Field for sorting on" + }, + "direction": { + "type": "string", + "description": "Sort order", + "pattern": "^(ASC|DESC)$" + } + } +} diff --git a/backend/embed/api_docs/components/StreamList.json b/backend/embed/api_docs/components/StreamList.json new file mode 100644 index 00000000..c3dae5ab --- /dev/null +++ b/backend/embed/api_docs/components/StreamList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "StreamList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StreamObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/StreamObject.json b/backend/embed/api_docs/components/StreamObject.json new file mode 100644 index 00000000..7141e861 --- /dev/null +++ b/backend/embed/api_docs/components/StreamObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "StreamObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "provider": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + } + } +} diff --git a/backend/embed/api_docs/components/TokenObject.json b/backend/embed/api_docs/components/TokenObject.json new file mode 100644 index 00000000..81c5d205 --- /dev/null +++ b/backend/embed/api_docs/components/TokenObject.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "description": "TokenObject", + "additionalProperties": false, + "required": ["expires", "token"], + "properties": { + "expires": { + "type": "number", + "description": "Token Expiry Unix Time", + "example": 1566540249, + "minimum": 1 + }, + "token": { + "type": "string", + "description": "JWT Token", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } +} diff --git a/backend/embed/api_docs/components/UserAuthObject.json b/backend/embed/api_docs/components/UserAuthObject.json new file mode 100644 index 00000000..b3ab4b48 --- /dev/null +++ b/backend/embed/api_docs/components/UserAuthObject.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "description": "UserAuthObject", + "additionalProperties": false, + "required": ["id", "user_id", "type", "created_on", "modified_on"], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "type": { + "type": "string", + "pattern": "^password$" + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + } + } +} diff --git a/backend/embed/api_docs/components/UserList.json b/backend/embed/api_docs/components/UserList.json new file mode 100644 index 00000000..a4d502f3 --- /dev/null +++ b/backend/embed/api_docs/components/UserList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "UserList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/UserObject.json b/backend/embed/api_docs/components/UserObject.json new file mode 100644 index 00000000..997f01a8 --- /dev/null +++ b/backend/embed/api_docs/components/UserObject.json @@ -0,0 +1,73 @@ +{ + "type": "object", + "description": "UserObject", + "additionalProperties": false, + "required": [ + "id", + "name", + "nickname", + "email", + "created_on", + "modified_on", + "is_disabled" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "email": { + "type": "string", + "minLength": 5, + "maxLength": 150 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "gravatar_url": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + }, + "is_deleted": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": ["type"], + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "pattern": "^password$" + } + } + }, + "capabilities": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + } +} diff --git a/backend/embed/api_docs/main.go b/backend/embed/api_docs/main.go new file mode 100644 index 00000000..95756af7 --- /dev/null +++ b/backend/embed/api_docs/main.go @@ -0,0 +1,9 @@ +package doc + +import "embed" + +// SwaggerFiles contain all the files used for swagger schema generation +//go:embed api.swagger.json +//go:embed components +//go:embed paths +var SwaggerFiles embed.FS diff --git a/backend/embed/api_docs/paths/certificates-authorities/caID/delete.json b/backend/embed/api_docs/paths/certificates-authorities/caID/delete.json new file mode 100644 index 00000000..3ae3bea8 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/caID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteCertificateAuthority", + "summary": "Delete a Certificate Authority", + "tags": [ + "Certificate Authorities" + ], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Certificate Authority", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/certificates-authorities/caID/get.json b/backend/embed/api_docs/paths/certificates-authorities/caID/get.json new file mode 100644 index 00000000..6bd4d008 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/caID/get.json @@ -0,0 +1,52 @@ +{ + "operationId": "getCertificateAuthority", + "summary": "Get a Certificate Authority object by ID", + "tags": ["Certificate Authorities"], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Certificate Authority", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1627531400, + "modified_on": 1627531400, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/caID/put.json b/backend/embed/api_docs/paths/certificates-authorities/caID/put.json new file mode 100644 index 00000000..63199bc6 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/caID/put.json @@ -0,0 +1,61 @@ +{ + "operationId": "updateCertificateAuthority", + "summary": "Update an existing Certificate Authority", + "tags": ["Certificate Authorities"], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Certificate Authority", + "example": 1 + } + ], + "requestBody": { + "description": "Certificate Authority details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateCertificateAuthority}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1627531400, + "modified_on": 1627531400, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/get.json b/backend/embed/api_docs/paths/certificates-authorities/get.json new file mode 100644 index 00000000..54138b75 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/get.json @@ -0,0 +1,92 @@ +{ + "operationId": "getCertificateAuthorities", + "summary": "Get a list of Certificate Authorities", + "tags": ["Certificate Authorities"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 2, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1627531400, + "modified_on": 1627531400, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_setup": true + }, + { + "id": 2, + "created_on": 1627531400, + "modified_on": 1627531400, + "name": "Let's Encrypt", + "acmesh_server": "https://acme-v02.api.letsencrypt.org/directory", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_setup": true + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/post.json b/backend/embed/api_docs/paths/certificates-authorities/post.json new file mode 100644 index 00000000..93d13797 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/post.json @@ -0,0 +1,48 @@ +{ + "operationId": "createCertificateAuthority", + "summary": "Create a new Certificate Authority", + "tags": ["Certificate Authorities"], + "requestBody": { + "description": "Certificate Authority to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateCertificateAuthority}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1627531400, + "modified_on": 1627531400, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates/certificateID/delete.json b/backend/embed/api_docs/paths/certificates/certificateID/delete.json new file mode 100644 index 00000000..98acfaf7 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/certificateID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteCertificate", + "summary": "Delete a Certificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the certificate", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a certificate that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/certificates/certificateID/get.json b/backend/embed/api_docs/paths/certificates/certificateID/get.json new file mode 100644 index 00000000..e26988ff --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/certificateID/get.json @@ -0,0 +1,61 @@ +{ + "operationId": "getCertificate", + "summary": "Get a certificate object by ID", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the certificate", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/certificates/certificateID/put.json b/backend/embed/api_docs/paths/certificates/certificateID/put.json new file mode 100644 index 00000000..3574d87b --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/certificateID/put.json @@ -0,0 +1,70 @@ +{ + "operationId": "updateCertificate", + "summary": "Update an existing Certificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the certificate", + "example": 1 + } + ], + "requestBody": { + "description": "Certificate details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateCertificate}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/certificates/get.json b/backend/embed/api_docs/paths/certificates/get.json new file mode 100644 index 00000000..d4cfcc79 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/get.json @@ -0,0 +1,90 @@ +{ + "operationId": "getCertificates", + "summary": "Get a list of certificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "is_ecc": 0, + "status": "ready" + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/certificates/post.json b/backend/embed/api_docs/paths/certificates/post.json new file mode 100644 index 00000000..5a332270 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/post.json @@ -0,0 +1,57 @@ +{ + "operationId": "createCertificate", + "summary": "Create a new Certificate", + "tags": [ + "Certificates" + ], + "requestBody": { + "description": "Certificate to create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateCertificate}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/config/get.json b/backend/embed/api_docs/paths/config/get.json new file mode 100644 index 00000000..ea1f0701 --- /dev/null +++ b/backend/embed/api_docs/paths/config/get.json @@ -0,0 +1,36 @@ +{ + "operationId": "config", + "summary": "Returns the API Service configuration", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/ConfigObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "data": "/data", + "log": { + "level": "debug", + "format": "nice" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/dns-providers/get.json b/backend/embed/api_docs/paths/dns-providers/get.json new file mode 100644 index 00000000..cb7aac8b --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/get.json @@ -0,0 +1,82 @@ +{ + "operationId": "getDNSProviders", + "summary": "Get a list of DNS Providers", + "tags": ["DNS Providers"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/post.json b/backend/embed/api_docs/paths/dns-providers/post.json new file mode 100644 index 00000000..a37d9cd2 --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/post.json @@ -0,0 +1,49 @@ +{ + "operationId": "createDNSProvider", + "summary": "Create a new DNS Provider", + "tags": ["DNS Providers"], + "requestBody": { + "description": "DNS Provider to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateDNSProvider}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/providerID/delete.json b/backend/embed/api_docs/paths/dns-providers/providerID/delete.json new file mode 100644 index 00000000..32b77b0d --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/providerID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteDNSProvider", + "summary": "Delete a DNS Provider", + "tags": [ + "DNS Providers" + ], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the DNS Provider", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a DNS Provider that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/dns-providers/providerID/get.json b/backend/embed/api_docs/paths/dns-providers/providerID/get.json new file mode 100644 index 00000000..eff1a367 --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/providerID/get.json @@ -0,0 +1,53 @@ +{ + "operationId": "getDNSProvider", + "summary": "Get a DNS Provider object by ID", + "tags": ["DNS Providers"], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the DNS Provider", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/providerID/put.json b/backend/embed/api_docs/paths/dns-providers/providerID/put.json new file mode 100644 index 00000000..f90d9ef3 --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/providerID/put.json @@ -0,0 +1,64 @@ +{ + "operationId": "updateDNSProvider", + "summary": "Update an existing DNS Provider", + "tags": ["DNS Providers"], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the DNS Provider", + "example": 1 + } + ], + "requestBody": { + "description": "DNS Provider details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateDNSProvider}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/get.json b/backend/embed/api_docs/paths/get.json new file mode 100644 index 00000000..0f9506f1 --- /dev/null +++ b/backend/embed/api_docs/paths/get.json @@ -0,0 +1,47 @@ +{ + "operationId": "health", + "summary": "Returns the API health status", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HealthObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": true, + "setup": true, + "error_reporting": true + } + } + }, + "unhealthy": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": false, + "setup": true, + "error_reporting": true + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/host-templates/get.json b/backend/embed/api_docs/paths/host-templates/get.json new file mode 100644 index 00000000..2c3ca700 --- /dev/null +++ b/backend/embed/api_docs/paths/host-templates/get.json @@ -0,0 +1,79 @@ +{ + "operationId": "getHostTemplates", + "summary": "Get a list of Host Templates", + "tags": ["Hosts"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostTemplateList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "created_on", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1646218093, + "modified_on": 1646218093, + "user_id": 1, + "name": "Default Proxy Template", + "host_type": "proxy", + "template": "# this is a proxy template" + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json b/backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json new file mode 100644 index 00000000..f14fe9d4 --- /dev/null +++ b/backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json @@ -0,0 +1,58 @@ +{ + "operationId": "deleteHostTemplate", + "summary": "Delete a Host Template", + "tags": ["Host Templates"], + "parameters": [ + { + "in": "path", + "name": "hostTemplateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Host Template", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a host template that is in use!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json b/backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json new file mode 100644 index 00000000..6f16a957 --- /dev/null +++ b/backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json @@ -0,0 +1,50 @@ +{ + "operationId": "getHostTemplate", + "summary": "Get a Host Template object by ID", + "tags": ["Hosts"], + "parameters": [ + { + "in": "path", + "name": "hostTemplateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host Template", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1646218093, + "modified_on": 1646218093, + "user_id": 1, + "name": "Default Host Template", + "host_type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json b/backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json new file mode 100644 index 00000000..2993e5a0 --- /dev/null +++ b/backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json @@ -0,0 +1,59 @@ +{ + "operationId": "updateHostTemplate", + "summary": "Update an existing Host Template", + "tags": ["Hosts"], + "parameters": [ + { + "in": "path", + "name": "hostTemplateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host Template", + "example": 1 + } + ], + "requestBody": { + "description": "Host Template details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateHostTemplate}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1646218093, + "modified_on": 1646218093, + "user_id": 1, + "name": "My renamed proxy template", + "host_type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/host-templates/post.json b/backend/embed/api_docs/paths/host-templates/post.json new file mode 100644 index 00000000..dd834c87 --- /dev/null +++ b/backend/embed/api_docs/paths/host-templates/post.json @@ -0,0 +1,46 @@ +{ + "operationId": "createHost", + "summary": "Create a new Host", + "tags": ["Hosts"], + "requestBody": { + "description": "Host to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateHostTemplate}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 10, + "created_on": 1646218093, + "modified_on": 1646218093, + "user_id": 1, + "name": "My proxy template", + "host_type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/get.json b/backend/embed/api_docs/paths/hosts/get.json new file mode 100644 index 00000000..160980e8 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/get.json @@ -0,0 +1,94 @@ +{ + "operationId": "getHosts", + "summary": "Get a list of Hosts", + "tags": ["Hosts"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "domain_names", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1646279455, + "modified_on": 1646279455, + "user_id": 2, + "type": "proxy", + "host_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "upstream_options": "", + "advanced_config": "", + "is_disabled": false + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/hostID/delete.json b/backend/embed/api_docs/paths/hosts/hostID/delete.json new file mode 100644 index 00000000..4df119ad --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteHost", + "summary": "Delete a Host", + "tags": [ + "Hosts" + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Host", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a host that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/hosts/hostID/get.json b/backend/embed/api_docs/paths/hosts/hostID/get.json new file mode 100644 index 00000000..654f7df9 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/get.json @@ -0,0 +1,65 @@ +{ + "operationId": "getHost", + "summary": "Get a Host object by ID", + "tags": ["Hosts"], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1646279455, + "modified_on": 1646279455, + "user_id": 2, + "type": "proxy", + "host_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "upstream_options": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/hostID/put.json b/backend/embed/api_docs/paths/hosts/hostID/put.json new file mode 100644 index 00000000..ec9675c8 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/put.json @@ -0,0 +1,74 @@ +{ + "operationId": "updateHost", + "summary": "Update an existing Host", + "tags": ["Hosts"], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host", + "example": 1 + } + ], + "requestBody": { + "description": "Host details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateHost}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1646279455, + "modified_on": 1646279455, + "user_id": 2, + "type": "proxy", + "host_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "upstream_options": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/post.json b/backend/embed/api_docs/paths/hosts/post.json new file mode 100644 index 00000000..e0362c68 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/post.json @@ -0,0 +1,61 @@ +{ + "operationId": "createHost", + "summary": "Create a new Host", + "tags": ["Hosts"], + "requestBody": { + "description": "Host to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateHost}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1645700556, + "modified_on": 1645700556, + "user_id": 2, + "type": "proxy", + "host_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "upstream_options": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/schema/get.json b/backend/embed/api_docs/paths/schema/get.json new file mode 100644 index 00000000..e21ae805 --- /dev/null +++ b/backend/embed/api_docs/paths/schema/get.json @@ -0,0 +1,9 @@ +{ + "operationId": "schema", + "summary": "Returns this swagger API schema", + "responses": { + "200": { + "description": "200 response" + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/settings/get.json b/backend/embed/api_docs/paths/settings/get.json new file mode 100644 index 00000000..3455b0ff --- /dev/null +++ b/backend/embed/api_docs/paths/settings/get.json @@ -0,0 +1,84 @@ +{ + "operationId": "getSettings", + "summary": "Get a list of settings", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1578010090, + "modified_on": 1578010095, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/settings/name/get.json b/backend/embed/api_docs/paths/settings/name/get.json new file mode 100644 index 00000000..775a4737 --- /dev/null +++ b/backend/embed/api_docs/paths/settings/name/get.json @@ -0,0 +1,55 @@ +{ + "operationId": "getSetting", + "summary": "Get a setting object by name", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "path", + "name": "name", + "schema": { + "type": "string", + "minLength": 2 + }, + "required": true, + "description": "Name of the setting", + "example": "default-site" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010095, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/settings/name/put.json b/backend/embed/api_docs/paths/settings/name/put.json new file mode 100644 index 00000000..ee741104 --- /dev/null +++ b/backend/embed/api_docs/paths/settings/name/put.json @@ -0,0 +1,64 @@ +{ + "operationId": "updateSetting", + "summary": "Update an existing Setting", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "path", + "name": "name", + "schema": { + "type": "string", + "minLength": 2 + }, + "required": true, + "description": "Name of the setting", + "example": "default-site" + } + ], + "requestBody": { + "description": "Setting details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateSetting}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010090, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/settings/post.json b/backend/embed/api_docs/paths/settings/post.json new file mode 100644 index 00000000..63921da2 --- /dev/null +++ b/backend/embed/api_docs/paths/settings/post.json @@ -0,0 +1,51 @@ +{ + "operationId": "createSetting", + "summary": "Create a new Setting", + "tags": [ + "Settings" + ], + "requestBody": { + "description": "Setting to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateSetting}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010090, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/streams/get.json b/backend/embed/api_docs/paths/streams/get.json new file mode 100644 index 00000000..482212d1 --- /dev/null +++ b/backend/embed/api_docs/paths/streams/get.json @@ -0,0 +1,75 @@ +{ + "operationId": "getStreams", + "summary": "Get a list of Streams", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + "TODO" + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/streams/post.json b/backend/embed/api_docs/paths/streams/post.json new file mode 100644 index 00000000..d1949830 --- /dev/null +++ b/backend/embed/api_docs/paths/streams/post.json @@ -0,0 +1,42 @@ +{ + "operationId": "createStream", + "summary": "Create a new Stream", + "tags": [ + "Streams" + ], + "requestBody": { + "description": "Host to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateStream}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/streams/streamID/delete.json b/backend/embed/api_docs/paths/streams/streamID/delete.json new file mode 100644 index 00000000..d0f35269 --- /dev/null +++ b/backend/embed/api_docs/paths/streams/streamID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteStream", + "summary": "Delete a Stream", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Stream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a Stream that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/streams/streamID/get.json b/backend/embed/api_docs/paths/streams/streamID/get.json new file mode 100644 index 00000000..94e54c3f --- /dev/null +++ b/backend/embed/api_docs/paths/streams/streamID/get.json @@ -0,0 +1,46 @@ +{ + "operationId": "getStream", + "summary": "Get a Stream object by ID", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Stream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/streams/streamID/put.json b/backend/embed/api_docs/paths/streams/streamID/put.json new file mode 100644 index 00000000..4baa882e --- /dev/null +++ b/backend/embed/api_docs/paths/streams/streamID/put.json @@ -0,0 +1,55 @@ +{ + "operationId": "updateStream", + "summary": "Update an existing Stream", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Stream", + "example": 1 + } + ], + "requestBody": { + "description": "Stream details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateStream}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/tokens/get.json b/backend/embed/api_docs/paths/tokens/get.json new file mode 100644 index 00000000..601697a7 --- /dev/null +++ b/backend/embed/api_docs/paths/tokens/get.json @@ -0,0 +1,37 @@ +{ + "operationId": "refreshToken", + "summary": "Refresh your access token", + "tags": [ + "Tokens" + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/tokens/post.json b/backend/embed/api_docs/paths/tokens/post.json new file mode 100644 index 00000000..44b95b2b --- /dev/null +++ b/backend/embed/api_docs/paths/tokens/post.json @@ -0,0 +1,79 @@ +{ + "operationId": "requestToken", + "summary": "Request a new access token from credentials", + "tags": [ + "Tokens" + ], + "requestBody": { + "description": "Credentials Payload", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.GetToken}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + }, + "403": { + "description": "403 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "error" + ], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 403, + "message": "Not available during setup phase" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/users/get.json b/backend/embed/api_docs/paths/users/get.json new file mode 100644 index 00000000..647b6d52 --- /dev/null +++ b/backend/embed/api_docs/paths/users/get.json @@ -0,0 +1,117 @@ +{ + "operationId": "getUsers", + "summary": "Get a list of users", + "tags": ["Users"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "name,nickname.desc,email.asc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 3, + "offset": 0, + "limit": 100, + "sort": [ + { + "field": "name", + "direction": "ASC" + }, + { + "field": "nickname", + "direction": "DESC" + }, + { + "field": "email", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010090, + "modified_on": 1578010095, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + }, + { + "id": 2, + "name": "John Doe", + "nickname": "John", + "email": "johdoe@example.com", + "created_on": 1578010100, + "modified_on": 1578010105, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": [ + "hosts.view", + "hosts.manage" + ] + }, + { + "id": 3, + "name": "Jane Doe", + "nickname": "Jane", + "email": "janedoe@example.com", + "created_on": 1578010110, + "modified_on": 1578010115, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": [ + "hosts.view", + "hosts.manage" + ] + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/post.json b/backend/embed/api_docs/paths/users/post.json new file mode 100644 index 00000000..7305f188 --- /dev/null +++ b/backend/embed/api_docs/paths/users/post.json @@ -0,0 +1,79 @@ +{ + "operationId": "createUser", + "summary": "Create a new User", + "tags": ["Users"], + "requestBody": { + "description": "User to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateUser}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010100, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "auth": { + "$ref": "#/components/schemas/UserAuthObject" + }, + "capabilities": ["full-admin"] + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "required": ["error"], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "An user already exists with this email address" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/auth/post.json b/backend/embed/api_docs/paths/users/userID/auth/post.json new file mode 100644 index 00000000..a71432f8 --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/auth/post.json @@ -0,0 +1,65 @@ +{ + "operationId": "setPassword", + "summary": "Set a User's password", + "tags": ["Users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to set yourself", + "example": 1 + } + ], + "requestBody": { + "description": "Credentials to set", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.SetAuth}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserAuthObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "user_id": 3, + "type": "password", + "created_on": 1648422222, + "modified_on": 1648423979 + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/delete.json b/backend/embed/api_docs/paths/users/userID/delete.json new file mode 100644 index 00000000..0ffa7024 --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteUser", + "summary": "Delete a User", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the user", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete yourself!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/embed/api_docs/paths/users/userID/get.json b/backend/embed/api_docs/paths/users/userID/get.json new file mode 100644 index 00000000..d5eebb03 --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/get.json @@ -0,0 +1,60 @@ +{ + "operationId": "getUser", + "summary": "Get a user object by ID or 'me'", + "tags": ["Users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to get yourself", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010105, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/put.json b/backend/embed/api_docs/paths/users/userID/put.json new file mode 100644 index 00000000..33dccd5c --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/put.json @@ -0,0 +1,107 @@ +{ + "operationId": "updateUser", + "summary": "Update an existing User", + "tags": ["Users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to update yourself", + "example": 1 + } + ], + "requestBody": { + "description": "User details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateUser}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010110, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "required": ["error"], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "duplicateemail": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "A user already exists with this email address" + } + } + }, + "nodisable": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot disable yourself!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/main.go b/backend/embed/main.go new file mode 100644 index 00000000..9909cef6 --- /dev/null +++ b/backend/embed/main.go @@ -0,0 +1,19 @@ +package embed + +import "embed" + +// APIDocFiles contain all the files used for swagger schema generation +//go:embed api_docs +var APIDocFiles embed.FS + +// Assets are frontend assets served from within this app +//go:embed assets +var Assets embed.FS + +// MigrationFiles are database migrations +//go:embed migrations/*.sql +var MigrationFiles embed.FS + +// NginxFiles hold nginx config templates +//go:embed nginx +var NginxFiles embed.FS diff --git a/backend/embed/migrations/20201013035318_initial_schema.sql b/backend/embed/migrations/20201013035318_initial_schema.sql new file mode 100644 index 00000000..417b5406 --- /dev/null +++ b/backend/embed/migrations/20201013035318_initial_schema.sql @@ -0,0 +1,209 @@ +-- migrate:up + +CREATE TABLE IF NOT EXISTS `user` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + nickname TEXT NOT NULL, + email TEXT NOT NULL, + is_system INTEGER NOT NULL DEFAULT 0, + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `capability` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS `user_has_capability` +( + user_id INTEGER NOT NULL, + capability_id INTEGER NOT NULL, + UNIQUE (user_id, capability_id), + FOREIGN KEY (capability_id) REFERENCES capability (id) +); + +CREATE TABLE IF NOT EXISTS `auth` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + secret TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + UNIQUE (user_id, type) +); + +CREATE TABLE IF NOT EXISTS `setting` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT "", + value TEXT NOT NULL, + UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS `audit_log` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + object_type TEXT NOT NULL, + object_id INTEGER NOT NULL, + action TEXT NOT NULL, + meta TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `certificate_authority` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + acmesh_server TEXT NOT NULL DEFAULT "", + ca_bundle TEXT NOT NULL DEFAULT "", + is_wildcard_supported INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards + max_domains INTEGER NOT NULL DEFAULT 5, -- per request + is_readonly INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `dns_provider` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + acmesh_name TEXT NOT NULL, + dns_sleep INTEGER NOT NULL DEFAULT 0, + meta TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `certificate` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + type TEXT NOT NULL, -- custom,dns,http + user_id INTEGER NOT NULL, + certificate_authority_id INTEGER, -- 0 for a custom cert + dns_provider_id INTEGER, -- 0, for a http or custom cert + name TEXT NOT NULL, + domain_names TEXT NOT NULL, + expires_on INTEGER DEFAULT 0, + status TEXT NOT NULL, -- ready,requesting,failed,provided + error_message text NOT NULL DEFAULT "", + meta TEXT NOT NULL, + is_ecc INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + FOREIGN KEY (certificate_authority_id) REFERENCES certificate_authority (id), + FOREIGN KEY (dns_provider_id) REFERENCES dns_provider (id) +); + +CREATE TABLE IF NOT EXISTS `stream` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + listen_interface TEXT NOT NULL, + incoming_port INTEGER NOT NULL, + upstream_options TEXT NOT NULL, + tcp_forwarding INTEGER NOT NULL DEFAULT 0, + udp_forwarding INTEGER NOT NULL DEFAULT 0, + advanced_config TEXT NOT NULL, + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `upstream` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + hosts TEXT NOT NULL, + balance_method TEXT NOT NULL, + max_fails INTEGER NOT NULL DEFAULT 1, + fail_timeout INTEGER NOT NULL DEFAULT 10, + advanced_config TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `access_list` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + meta TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `host_template` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + host_type TEXT NOT NULL, + template TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `host` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + host_template_id INTEGER NOT NULL, + listen_interface TEXT NOT NULL, + domain_names TEXT NOT NULL, + upstream_id INTEGER NOT NULL, + certificate_id INTEGER, + access_list_id INTEGER, + ssl_forced INTEGER NOT NULL DEFAULT 0, + caching_enabled INTEGER NOT NULL DEFAULT 0, + block_exploits INTEGER NOT NULL DEFAULT 0, + allow_websocket_upgrade INTEGER NOT NULL DEFAULT 0, + http2_support INTEGER NOT NULL DEFAULT 0, + hsts_enabled INTEGER NOT NULL DEFAULT 0, + hsts_subdomains INTEGER NOT NULL DEFAULT 0, + paths TEXT NOT NULL, + upstream_options TEXT NOT NULL DEFAULT "", + advanced_config TEXT NOT NULL DEFAULT "", + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + FOREIGN KEY (host_template_id) REFERENCES host_template (id), + FOREIGN KEY (upstream_id) REFERENCES upstream (id), + FOREIGN KEY (certificate_id) REFERENCES certificate (id), + FOREIGN KEY (access_list_id) REFERENCES access_list (id) +); + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/embed/migrations/20201013035839_initial_data.sql b/backend/embed/migrations/20201013035839_initial_data.sql new file mode 100644 index 00000000..fa71871e --- /dev/null +++ b/backend/embed/migrations/20201013035839_initial_data.sql @@ -0,0 +1,171 @@ +-- migrate:up + +-- User permissions +INSERT INTO `capability` ( + name +) VALUES + ("full-admin"), + ("access-lists.view"), + ("access-lists.manage"), + ("audit-log.view"), + ("certificates.view"), + ("certificates.manage"), + ("certificate-authorities.view"), + ("certificate-authorities.manage"), + ("dns-providers.view"), + ("dns-providers.manage"), + ("hosts.view"), + ("hosts.manage"), + ("host-templates.view"), + ("host-templates.manage"), + ("settings.manage"), + ("streams.view"), + ("streams.manage"), + ("users.manage"); + +-- Default error reporting setting +INSERT INTO `setting` ( + created_on, + modified_on, + name, + description, + value +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "error-reporting", + "If enabled, any application errors are reported to Sentry. Sensitive information is not sent.", + "true" -- remember this is json +); + +-- Default site +INSERT INTO `setting` ( + created_on, + modified_on, + name, + description, + value +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "default-site", + "What to show users who hit your Nginx server by default", + '"welcome"' -- remember this is json +); + +-- Default Certificate Authorities + +INSERT INTO `certificate_authority` ( + created_on, + modified_on, + name, + acmesh_server, + is_wildcard_supported, + max_domains, + is_readonly +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "ZeroSSL", + "zerossl", + 1, + 10, + 1 +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Let's Encrypt", + "https://acme-v02.api.letsencrypt.org/directory", + 1, + 10, + 1 +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Buypass Go SSL", + "https://api.buypass.com/acme/directory", + 0, + 5, + 1 +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Let's Encrypt (Testing)", + "https://acme-staging-v02.api.letsencrypt.org/directory", + 1, + 10, + 1 +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Buypass Go SSL (Testing)", + "https://api.test4.buypass.no/acme/directory", + 0, + 5, + 1 +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "SSL.com", + "ssl.com", + 0, + 10, + 1 +); + +-- System User +INSERT INTO `user` ( + created_on, + modified_on, + name, + nickname, + email, + is_system +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "System", + "System", + "system@localhost", + 1 +); + +-- Host Templates +INSERT INTO `host_template` ( + created_on, + modified_on, + user_id, + name, + host_type, + template +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Proxy Template", + "proxy", + "# this is a proxy template" +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Redirect Template", + "redirect", + "# this is a redirect template" +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Dead Template", + "dead", + "# this is a dead template" +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Stream Template", + "stream", + "# this is a stream template" +); + +-- migrate:down diff --git a/backend/embed/nginx/_assets.conf.hbs b/backend/embed/nginx/_assets.conf.hbs new file mode 100644 index 00000000..73639767 --- /dev/null +++ b/backend/embed/nginx/_assets.conf.hbs @@ -0,0 +1,4 @@ +{{#if caching_enabled}} + # Asset Caching + include conf.d/include/assets.conf; +{{/if}} diff --git a/backend/embed/nginx/_certificates.conf.hbs b/backend/embed/nginx/_certificates.conf.hbs new file mode 100644 index 00000000..d114f982 --- /dev/null +++ b/backend/embed/nginx/_certificates.conf.hbs @@ -0,0 +1,13 @@ +{{#if certificate}} + {{#if (equal certificate.certificate_authority_id "0")}} + # Custom SSL + ssl_certificate {{npm_data_dir}}/custom_ssl/npm-{{certificate.id}}/fullchain.pem; + ssl_certificate_key {{npm_data_dir}}/custom_ssl/npm-{{certificate.id}}/privkey.pem; + {{else}} + # Acme SSL + include {{nginx_conf_dir}}/npm/conf.d/acme-challenge.conf; + include {{nginx_conf_dir}}/npm/conf.d/include/ssl-ciphers.conf; + ssl_certificate {{acme_certs_dir}}/npm-{{certificate.id}}/fullchain.pem; + ssl_certificate_key {{acme_certs_dir}}/npm-{{certificate.id}}/privkey.pem; + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_forced_ssl.conf.hbs b/backend/embed/nginx/_forced_ssl.conf.hbs new file mode 100644 index 00000000..970296e0 --- /dev/null +++ b/backend/embed/nginx/_forced_ssl.conf.hbs @@ -0,0 +1,6 @@ +{{#if certificate}} + {{#if ssl_forced}} + # Force SSL + include {{nginx_conf_dir}}/npm/conf.d/include/force-ssl.conf; + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_hsts.conf.hbs b/backend/embed/nginx/_hsts.conf.hbs new file mode 100644 index 00000000..c27da5aa --- /dev/null +++ b/backend/embed/nginx/_hsts.conf.hbs @@ -0,0 +1,8 @@ +{{#if certificate}} + {{#if ssl_forced}} + {{#if hsts_enabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if hsts_subdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_listen.conf.hbs b/backend/embed/nginx/_listen.conf.hbs new file mode 100644 index 00000000..217da00f --- /dev/null +++ b/backend/embed/nginx/_listen.conf.hbs @@ -0,0 +1,18 @@ +listen 80; + +{{#if ipv6}} + listen [::]:80; +{{else}} + #listen [::]:80; +{{/if}} + +{{#if certificate}} + listen 443 ssl{% if http2_support %} http2{% endif %}; + {{#if ipv6}} + listen [::]:443; + {{else}} + #listen [::]:443; + {{/if}} +{{/if}} + +server_name{{#each domain_names}} {{this}}{{/each}}; diff --git a/backend/embed/nginx/_location.conf.hbs b/backend/embed/nginx/_location.conf.hbs new file mode 100644 index 00000000..167d46eb --- /dev/null +++ b/backend/embed/nginx/_location.conf.hbs @@ -0,0 +1,40 @@ +location {{path}} { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass {{forward_scheme}}://{{forward_host}}:{{forward_port}}{{forward_path}}; + + {{#if access_list}} + {{#if access_list.items}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file {{npm_data_dir}}/access/{{access_list.id}}; + {{access_list.passauth}} + {{/if}} + + # Access Rules + {{#each access_list.clients as |client clientIdx|}} + {{client.rule}}; + {{/each}}deny all; + + # Access checks must... + {{#if access_list.satisfy}} + {{access_list.satisfy}}; + {{/if}} + {{/if}} + + {{> inc_assets}} + {{> inc_forced_ssl}} + {{> inc_hsts}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + {{advanced_config}} + } + diff --git a/backend/embed/nginx/acme-request.conf.hbs b/backend/embed/nginx/acme-request.conf.hbs new file mode 100644 index 00000000..aac14895 --- /dev/null +++ b/backend/embed/nginx/acme-request.conf.hbs @@ -0,0 +1,15 @@ +server { + listen 80; + {{#if ipv6}} + listen [::]:80; + {{/if}} + + server_name{{#each domain_names}} {{this}}{{/each}}; + access_log {{npm_data_dir}}/logs/acme-requests_access.log standard; + error_log {{npm_data_dir}}/logs/acme-requests_error.log warn; + {{nginx_conf_dir}}/npm/conf.d/include/letsencrypt-acme-challenge.conf; + + location / { + return 404; + } +} diff --git a/backend/embed/nginx/dead_host.conf.hbs b/backend/embed/nginx/dead_host.conf.hbs new file mode 100644 index 00000000..289940c8 --- /dev/null +++ b/backend/embed/nginx/dead_host.conf.hbs @@ -0,0 +1,20 @@ +{{#if enabled}} + server { + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + access_log {{npm_data_dir}}/logs/dead-host-{{id}}_access.log standard; + error_log {{npm_data_dir}}/logs/dead-host-{{id}}_error.log warn; + + {{advanced_config}} + + {{#if use_default_location}} + location / { + {{> inc_hsts}} + return 404; + } + {{/if}} + } +{{/if}} diff --git a/backend/embed/nginx/default.conf.hbs b/backend/embed/nginx/default.conf.hbs new file mode 100644 index 00000000..190ec02b --- /dev/null +++ b/backend/embed/nginx/default.conf.hbs @@ -0,0 +1,35 @@ +{{#if (equal value "congratulations")}} + # Skipping output, congratulations page configration is baked in. +{{else}} + server { + listen 80 default; + {{#if ipv6}} + listen [::]:80; + {{else}} + #listen [::]:80; + {{/if}} + + server_name default-host.localhost; + access_log {{npm_data_dir}}/logs/default-host_access.log combined; + error_log {{npm_data_dir}}/logs/default-host_error.log warn; + + {{#if (equal value "404")}} + location / { + return 404; + } + {{/if}} + + {{#if (equal value "redirect")}} + location / { + return 301 {{meta.redirect}}; + } + {{/if}} + + {{#if (equal value "html")}} + root {{npm_data_dir}}/nginx/default_www; + location / { + try_files $uri /index.html; + } + {{/if}} + } +{{/if}} diff --git a/backend/embed/nginx/ip_ranges.conf.hbs b/backend/embed/nginx/ip_ranges.conf.hbs new file mode 100644 index 00000000..7b7c3d07 --- /dev/null +++ b/backend/embed/nginx/ip_ranges.conf.hbs @@ -0,0 +1,3 @@ +{{#each ip_ranges as |range rangeIdx|}} + set_real_ip_from {{range}}; +{{/each}} diff --git a/backend/embed/nginx/proxy_host.conf.hbs b/backend/embed/nginx/proxy_host.conf.hbs new file mode 100644 index 00000000..e7681f4b --- /dev/null +++ b/backend/embed/nginx/proxy_host.conf.hbs @@ -0,0 +1,62 @@ +{{#if enabled}} + server { + set $forward_scheme {{forward_scheme}}; + set $server "{{forward_host}}"; + set $port {{forward_port}}; + + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_assets}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log {{npm_data_dir}}/logs/proxy-host-{{id}}_access.log proxy; + error_log {{npm_data_dir}}/logs/proxy-host-{{id}}_error.log warn; + + {{advanced_config}} + {{locations}} + + {{#if use_default_location}} + location / { + {{#if access_list}} + {{#if access_list.items}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file {{npm_data_dir}}/access/{{access_list.id}}; + {{access_list.passauth}} + {{/if}} + + # Access Rules + {{#each access_list.clients as |client clientIdx|}} + {{client.rule}}; + {{/each}}deny all; + + # Access checks must... + {{#if access_list.satisfy}} + {{access_list.satisfy}}; + {{/if}} + {{/if}} + + {{> inc_hsts}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + # Proxy! + include {{nginx_conf_dir}}/npm/conf.d/include/proxy.conf; + } + {{/if}} + + # Custom + include {{npm_data_dir}}/nginx/custom/server_proxy[.]conf; + } +{{/if}} diff --git a/backend/embed/nginx/redirection_host.conf.hbs b/backend/embed/nginx/redirection_host.conf.hbs new file mode 100644 index 00000000..18208c6c --- /dev/null +++ b/backend/embed/nginx/redirection_host.conf.hbs @@ -0,0 +1,28 @@ +{{#if enabled}} + server { + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_assets}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + access_log {{npm_data_dir}}/logs/redirection-host-{{ id }}_access.log standard; + error_log {{npm_data_dir}}/logs/redirection-host-{{ id }}_error.log warn; + + {{advanced_config}} + + {{#if use_default_location}} + location / { + {{> inc_hsts}} + {{#if preserve_path}} + return {{forward_http_code}} {{forward_scheme}}://{{forward_domain_name}}$request_uri; + {{else}} + return {{forward_http_code}} {{forward_scheme}}://{{forward_domain_name}}; + {{/if}} + } + {{/if}} + + # Custom + include {{npm_data_dir}}/nginx/custom/server_redirect[.]conf; + } +{{/if}} diff --git a/backend/embed/nginx/stream.conf.hbs b/backend/embed/nginx/stream.conf.hbs new file mode 100644 index 00000000..bc85bbfa --- /dev/null +++ b/backend/embed/nginx/stream.conf.hbs @@ -0,0 +1,34 @@ +{{#if enabled}} + {{#if tcp_forwarding}} + server { + listen {{incoming_port}}; + {{#if ipv6}} + listen [::]:{{incoming_port}}; + {{else}} + #listen [::]:{{incoming_port}}; + {{/if}} + + proxy_pass {{forward_ip}}:{{forwarding_port}}; + + # Custom + include {{npm_data_dir}}/nginx/custom/server_stream[.]conf; + include {{npm_data_dir}}/nginx/custom/server_stream_tcp[.]conf; + } + {{/if}} + + {{#if udp_forwarding}} + server { + listen {{incoming_port}} udp; + {{#if ipv6}} + listen [::]:{{ incoming_port }} udp; + {{else}} + #listen [::]:{{incoming_port}} udp; + {{/if}} + proxy_pass {{forward_ip}}:{{forwarding_port}}; + + # Custom + include {{npm_data_dir}}/nginx/custom/server_stream[.]conf; + include {{npm_data_dir}}/nginx/custom/server_stream_udp[.]conf; + } + {{/if}} +{{/if}} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..45118bf3 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,39 @@ +module npm + +go 1.17 + +require ( + github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e + github.com/fatih/color v1.13.0 + github.com/getsentry/sentry-go v0.12.0 + github.com/go-chi/chi v4.1.2+incompatible + github.com/go-chi/cors v1.2.0 + github.com/go-chi/jwtauth v4.0.4+incompatible + github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 + github.com/jmoiron/sqlx v1.3.4 + github.com/mattn/go-sqlite3 v2.0.3+incompatible + github.com/qri-io/jsonschema v0.2.1 + github.com/stretchr/testify v1.7.0 + github.com/vrischmann/envconfig v1.3.0 + golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 +) + +require ( + github.com/alexflint/go-arg v1.4.3 // indirect + github.com/alexflint/go-scalar v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c // indirect + github.com/lestrrat-go/option v1.0.0 // indirect + github.com/lestrrat-go/pdebug/v3 v3.0.1 // indirect + github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/qri-io/jsonpointer v0.1.1 // indirect + golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..f980fa1c --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,300 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= +github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= +github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e h1:2R8DvYLNr5DL25eWwpOdPno1eIbTNjJC0d7v8ti5cus= +github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e/go.mod h1:YjikoytuRI4q+GRd3xrOrKJN+Ayi2dwRomHLDDeMHfs= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= +github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= +github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE= +github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY= +github.com/go-chi/jwtauth v4.0.4+incompatible/go.mod h1:Q5EIArY/QnD6BdS+IyDw7B2m6iNbnPxtfd6/BcmtWbs= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k= +github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kyoh86/richgo v0.3.8/go.mod h1:2C8POkF1H04iTOG2Tp1yyZhspCME9nN3cir3VXJ02II= +github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c= +github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8= +github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= +github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b h1:YUFRoeHK/mvRjBR0bBRDC7ZGygYchoQ8j1xMENlObro= +github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b/go.mod h1:s2U6PowV3/Jobkx/S9d0XiPwOzs6niW3DIouw+7nZC8= +github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb h1:DDg5u5lk2v8O8qxs8ecQkMUBj3tLW6wkSLzxxOyi1Ig= +github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb/go.mod h1:i+E8Uf04vf2QjOWyJdGY75vmG+4rxiZW2kIj1lTB5mo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= +github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= +github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0= +github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= +github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= +github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index 8d42d096..00000000 --- a/backend/index.js +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -const logger = require('./logger').global; - -async function appStart () { - // Create config file db settings if environment variables have been set - await createDbConfigFromEnvironment(); - - const migrate = require('./migrate'); - const setup = require('./setup'); - const app = require('./app'); - const apiValidator = require('./lib/validator/api'); - const internalCertificate = require('./internal/certificate'); - const internalIpRanges = require('./internal/ip_ranges'); - - return migrate.latest() - .then(setup) - .then(() => { - return apiValidator.loadSchemas; - }) - .then(internalIpRanges.fetch) - .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(err.message); - setTimeout(appStart, 1000); - }); -} - -async function createDbConfigFromEnvironment() { - return new Promise((resolve, reject) => { - const envMysqlHost = process.env.DB_MYSQL_HOST || null; - const envMysqlPort = process.env.DB_MYSQL_PORT || null; - const envMysqlUser = process.env.DB_MYSQL_USER || null; - const envMysqlName = process.env.DB_MYSQL_NAME || null; - let envSqliteFile = process.env.DB_SQLITE_FILE || null; - - const fs = require('fs'); - const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json'; - let configData = {}; - - try { - configData = require(filename); - } catch (err) { - // do nothing - } - - if (configData.database && configData.database.engine && !configData.database.fromEnv) { - logger.info('Manual db configuration already exists, skipping config creation from environment variables'); - resolve(); - return; - } - - if ((!envMysqlHost || !envMysqlPort || !envMysqlUser || !envMysqlName) && !envSqliteFile){ - envSqliteFile = '/data/database.sqlite'; - logger.info(`No valid environment variables for database provided, using default SQLite file '${envSqliteFile}'`); - } - - if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) { - const newConfig = { - fromEnv: true, - engine: 'mysql', - host: envMysqlHost, - port: envMysqlPort, - user: envMysqlUser, - password: process.env.DB_MYSQL_PASSWORD, - name: envMysqlName, - }; - - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating MySQL knex configuration from environment variables'); - configData.database = newConfig; - - } else { - const newConfig = { - fromEnv: true, - engine: 'knex-native', - knex: { - client: 'sqlite3', - connection: { - filename: envSqliteFile - }, - useNullAsDefault: true - } - }; - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating SQLite knex configuration'); - configData.database = newConfig; - } - - // Write config - fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => { - if (err) { - logger.error('Could not write db config to config file: ' + filename); - reject(err); - } else { - logger.debug('Wrote db configuration to config file: ' + filename); - resolve(); - } - }); - }); -} - -try { - appStart(); -} catch (err) { - logger.error(err.message, err); - process.exit(1); -} - diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js deleted file mode 100644 index 083bfa62..00000000 --- a/backend/internal/access-list.js +++ /dev/null @@ -1,534 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const batchflow = require('batchflow'); -const logger = require('../logger').access; -const error = require('../lib/error'); -const accessListModel = require('../models/access_list'); -const accessListAuthModel = require('../models/access_list_auth'); -const accessListClientModel = require('../models/access_list_client'); -const proxyHostModel = require('../models/proxy_host'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const utils = require('../lib/utils'); - -function omissions () { - return ['is_deleted']; -} - -const internalAccessList = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - return access.can('access_lists:create', data) - .then((/*access_data*/) => { - return accessListModel - .query() - .omit(omissions()) - .insertAndFetch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - owner_user_id: access.token.getUserId(1) - }); - }) - .then((row) => { - data.id = row.id; - - let promises = []; - - // Now add the items - data.items.map((item) => { - promises.push(accessListAuthModel - .query() - .insert({ - access_list_id: row.id, - username: item.username, - password: item.password - }) - ); - }); - - // Now add the clients - if (typeof data.clients !== 'undefined' && data.clients) { - data.clients.map((client) => { - promises.push(accessListClientModel - .query() - .insert({ - access_list_id: row.id, - address: client.address, - directive: client.directive - }) - ); - }); - } - - return Promise.all(promises); - }) - .then(() => { - // re-fetch with expansions - return internalAccessList.get(access, { - id: data.id, - expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] - }, true /* <- skip masking */); - }) - .then((row) => { - // Audit log - data.meta = _.assign({}, data.meta || {}, row.meta); - - return internalAccessList.build(row) - .then(() => { - if (row.proxy_host_count) { - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - } - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'access-list', - object_id: row.id, - meta: internalAccessList.maskItems(data) - }); - }) - .then(() => { - return internalAccessList.maskItems(row); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.name] - * @param {String} [data.items] - * @return {Promise} - */ - update: (access, data) => { - return access.can('access_lists:update', data.id) - .then((/*access_data*/) => { - return internalAccessList.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - }) - .then(() => { - // patch name if specified - if (typeof data.name !== 'undefined' && data.name) { - return accessListModel - .query() - .where({id: data.id}) - .patch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - }); - } - }) - .then(() => { - // Check for items and add/update/remove them - if (typeof data.items !== 'undefined' && data.items) { - let promises = []; - let items_to_keep = []; - - data.items.map(function (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 - items_to_keep.push(item.username); - } - }); - - let query = accessListAuthModel - .query() - .delete() - .where('access_list_id', data.id); - - if (items_to_keep.length) { - query.andWhere('username', 'NOT IN', items_to_keep); - } - - return query - .then(() => { - // Add new items - if (promises.length) { - return Promise.all(promises); - } - }); - } - }) - .then(() => { - // Check for clients and add/update/remove them - if (typeof data.clients !== 'undefined' && data.clients) { - let promises = []; - - data.clients.map(function (client) { - if (client.address) { - promises.push(accessListClientModel - .query() - .insert({ - access_list_id: data.id, - address: client.address, - directive: client.directive - }) - ); - } - }); - - let query = accessListClientModel - .query() - .delete() - .where('access_list_id', data.id); - - return query - .then(() => { - // Add new items - if (promises.length) { - return Promise.all(promises); - } - }); - } - }) - .then(internalNginx.reload) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'access-list', - object_id: data.id, - meta: internalAccessList.maskItems(data) - }); - }) - .then(() => { - // re-fetch with expansions - return internalAccessList.get(access, { - id: data.id, - expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] - }, true /* <- skip masking */); - }) - .then((row) => { - return internalAccessList.build(row) - .then(() => { - if (row.proxy_host_count) { - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - } - }) - .then(() => { - return internalAccessList.maskItems(row); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @param {Boolean} [skip_masking] - * @return {Promise} - */ - get: (access, data, skip_masking) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('access_lists:get', data.id) - .then((access_data) => { - let query = accessListModel - .query() - .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) - .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') - .where('access_list.is_deleted', 0) - .andWhere('access_list.id', data.id) - .allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]') - .omit(['access_list.is_deleted']) - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - if (!skip_masking && typeof row.items !== 'undefined' && row.items) { - row = internalAccessList.maskItems(row); - } - - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('access_lists:delete', data.id) - .then(() => { - return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']}); - }) - .then((row) => { - if (!row) { - throw new error.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 - return accessListModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // 2. update any proxy hosts that were using it (ignoring permissions) - if (row.proxy_hosts) { - return proxyHostModel - .query() - .where('access_list_id', '=', row.id) - .patch({access_list_id: 0}) - .then(() => { - // 3. reconfigure those hosts, then reload nginx - - // set the access_list_id to zero for these items - row.proxy_hosts.map(function (val, idx) { - row.proxy_hosts[idx].access_list_id = 0; - }); - - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - }) - .then(() => { - return internalNginx.reload(); - }); - } - }) - .then(() => { - // delete the htpasswd file - let htpasswd_file = internalAccessList.getFilename(row); - - try { - fs.unlinkSync(htpasswd_file); - } catch (err) { - // do nothing - } - }) - .then(() => { - // 4. audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'access-list', - object_id: row.id, - meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts']) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Lists - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('access_lists:list') - .then((access_data) => { - let query = accessListModel - .query() - .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) - .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') - .where('access_list.is_deleted', 0) - .groupBy('access_list.id') - .omit(['access_list.is_deleted']) - .allowEager('[owner,items,clients]') - .orderBy('access_list.name', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .then((rows) => { - if (rows) { - rows.map(function (row, idx) { - if (typeof row.items !== 'undefined' && row.items) { - rows[idx] = internalAccessList.maskItems(row); - } - }); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Integer} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = accessListModel - .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 parseInt(row.count, 10); - }); - }, - - /** - * @param {Object} list - * @returns {Object} - */ - maskItems: (list) => { - if (list && typeof list.items !== 'undefined') { - list.items.map(function (val, idx) { - let repeat_for = 8; - let first_char = '*'; - - if (typeof val.password !== 'undefined' && val.password) { - repeat_for = val.password.length - 1; - first_char = val.password.charAt(0); - } - - list.items[idx].hint = first_char + ('*').repeat(repeat_for); - list.items[idx].password = ''; - }); - } - - 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: (list) => { - logger.info('Building Access file #' + list.id + ' for: ' + list.name); - - return new Promise((resolve, reject) => { - let htpasswd_file = internalAccessList.getFilename(list); - - // 1. remove any existing access file - try { - fs.unlinkSync(htpasswd_file); - } catch (err) { - // do nothing - } - - // 2. create empty access file - try { - fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'}); - resolve(htpasswd_file); - } catch (err) { - reject(err); - } - }) - .then((htpasswd_file) => { - // 3. generate password for each user - if (list.items.length) { - return new Promise((resolve, reject) => { - batchflow(list.items).sequential() - .each((i, item, next) => { - if (typeof item.password !== 'undefined' && item.password.length) { - logger.info('Adding: ' + item.username); - - utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"') - .then((/*result*/) => { - 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); - }); - }); - } - }); - } -}; - -module.exports = internalAccessList; diff --git a/backend/internal/acme/acmesh.go b/backend/internal/acme/acmesh.go new file mode 100644 index 00000000..462eba39 --- /dev/null +++ b/backend/internal/acme/acmesh.go @@ -0,0 +1,200 @@ +package acme + +// Some light reading: +// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "npm/internal/config" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/logger" +) + +func getAcmeShFilePath() (string, error) { + path, err := exec.LookPath("acme.sh") + if err != nil { + return path, fmt.Errorf("Cannot find acme.sh execuatable script in PATH") + } + return path, nil +} + +func getCommonEnvVars() []string { + return []string{ + fmt.Sprintf("ACMESH_CONFIG_HOME=%s", os.Getenv("ACMESH_CONFIG_HOME")), + fmt.Sprintf("ACMESH_HOME=%s", os.Getenv("ACMESH_HOME")), + fmt.Sprintf("CERT_HOME=%s", os.Getenv("CERT_HOME")), + fmt.Sprintf("LE_CONFIG_HOME=%s", os.Getenv("LE_CONFIG_HOME")), + fmt.Sprintf("LE_WORKING_DIR=%s", os.Getenv("LE_WORKING_DIR")), + } +} + +// GetAcmeShVersion will return the acme.sh script version +func GetAcmeShVersion() string { + if r, err := shExec([]string{"--version"}, nil); err == nil { + // modify the output + r = strings.Trim(r, "\n") + v := strings.Split(r, "\n") + return v[len(v)-1] + } + return "" +} + +// CreateAccountKey is required for each server initially +func CreateAccountKey(ca *certificateauthority.Model) error { + args := []string{"--create-account-key", "--accountkeylength", "2048"} + if ca != nil { + logger.Info("Acme.sh CreateAccountKey for %s", ca.AcmeshServer) + args = append(args, "--server", ca.AcmeshServer) + if ca.CABundle != "" { + args = append(args, "--ca-bundle", ca.CABundle) + } + } else { + logger.Info("Acme.sh CreateAccountKey") + } + + args = append(args, getCommonArgs()...) + ret, err := shExec(args, nil) + if err != nil { + return err + } + + logger.Debug("CreateAccountKey returned:\n%+v", ret) + + return nil +} + +// RequestCert does all the heavy lifting +func RequestCert(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) (string, error) { + args, err := buildCertRequestArgs(domains, method, outputFullchainFile, outputKeyFile, dnsProvider, ca, force) + if err != nil { + return err.Error(), err + } + + envs := make([]string, 0) + if dnsProvider != nil { + envs, err = dnsProvider.GetAcmeShEnvVars() + if err != nil { + return err.Error(), err + } + } + + ret, err := shExec(args, envs) + if err != nil { + return ret, err + } + + return "", nil +} + +// shExec executes the acme.sh with arguments +func shExec(args []string, envs []string) (string, error) { + acmeSh, err := getAcmeShFilePath() + if err != nil { + logger.Error("AcmeShError", err) + return "", err + } + + logger.Debug("CMD: %s %v", acmeSh, args) + // nolint: gosec + c := exec.Command(acmeSh, args...) + c.Env = append(getCommonEnvVars(), envs...) + + b, e := c.Output() + + if e != nil { + logger.Error("AcmeShError", fmt.Errorf("Command error: %s -- %v\n%+v", acmeSh, args, e)) + logger.Warn(string(b)) + } + + return string(b), e +} + +func getCommonArgs() []string { + args := make([]string, 0) + + if config.Configuration.Acmesh.Home != "" { + args = append(args, "--home", config.Configuration.Acmesh.Home) + } + if config.Configuration.Acmesh.ConfigHome != "" { + args = append(args, "--config-home", config.Configuration.Acmesh.ConfigHome) + } + if config.Configuration.Acmesh.CertHome != "" { + args = append(args, "--cert-home", config.Configuration.Acmesh.CertHome) + } + + args = append(args, "--log", "/data/logs/acme.sh.log") + args = append(args, "--debug", "2") + + return args +} + +// This is split out into it's own function so it's testable +func buildCertRequestArgs(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) ([]string, error) { + // The argument order matters. + // see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode + // for multiple domains and note that the method of validation is required just after the domain arg, each time. + + // TODO log file location configurable + args := []string{"--issue"} + + if ca != nil { + args = append(args, "--server", ca.AcmeshServer) + if ca.CABundle != "" { + args = append(args, "--ca-bundle", ca.CABundle) + } + } + + if outputFullchainFile != "" { + args = append(args, "--fullchain-file", outputFullchainFile) + } + + if outputKeyFile != "" { + args = append(args, "--key-file", outputKeyFile) + } + + methodArgs := make([]string, 0) + switch method { + case "dns": + if dnsProvider == nil { + return nil, ErrDNSNeedsDNSProvider + } + methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeshName) + if dnsProvider.DNSSleep > 0 { + // See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck + methodArgs = append(methodArgs, "--dnssleep", fmt.Sprintf("%d", dnsProvider.DNSSleep)) + } + + case "http": + if dnsProvider != nil { + return nil, ErrHTTPHasDNSProvider + } + methodArgs = append(methodArgs, "-w", config.Configuration.Acmesh.GetWellknown()) + default: + return nil, ErrMethodNotSupported + } + + hasMethod := false + + // Add domains to args + for _, domain := range domains { + args = append(args, "-d", domain) + // Method has to appear after each domain + if !hasMethod { + args = append(args, methodArgs...) + hasMethod = true + } + } + + if force { + args = append(args, "--force") + } + + args = append(args, getCommonArgs()...) + + return args, nil +} diff --git a/backend/internal/acme/acmesh_test.go b/backend/internal/acme/acmesh_test.go new file mode 100644 index 00000000..837aa365 --- /dev/null +++ b/backend/internal/acme/acmesh_test.go @@ -0,0 +1,204 @@ +package acme + +import ( + "fmt" + "testing" + + "npm/internal/config" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + + "github.com/stretchr/testify/assert" +) + +// Tear up/down +/* +func TestMain(m *testing.M) { + config.Init(&version, &commit, &sentryDSN) + code := m.Run() + os.Exit(code) +} +*/ + +// TODO configurable +const acmeLogFile = "/data/logs/acme.sh.log" + +func TestBuildCertRequestArgs(t *testing.T) { + type want struct { + args []string + err error + } + + wellknown := config.Configuration.Acmesh.GetWellknown() + exampleKey := fmt.Sprintf("%s/example.com.key", config.Configuration.Acmesh.CertHome) + exampleChain := fmt.Sprintf("%s/a.crt", config.Configuration.Acmesh.CertHome) + + tests := []struct { + name string + domains []string + method string + outputFullchainFile string + outputKeyFile string + dnsProvider *dnsprovider.Model + ca *certificateauthority.Model + want want + }{ + { + name: "http single domain", + domains: []string{"example.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "-w", + wellknown, + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "http multiple domains", + domains: []string{"example.com", "example-two.com", "example-three.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "-w", + wellknown, + "-d", + "example-two.com", + "-d", + "example-three.com", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "http single domain with dns provider", + domains: []string{"example.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: nil, + err: ErrHTTPHasDNSProvider, + }, + }, + { + name: "dns single domain", + domains: []string{"example.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "--dns", + "dns_cf", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "dns multiple domains", + domains: []string{"example.com", "example-two.com", "example-three.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "--dns", + "dns_cf", + "-d", + "example-two.com", + "-d", + "example-three.com", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "dns single domain no provider", + domains: []string{"example.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: nil, + err: ErrDNSNeedsDNSProvider, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, err := buildCertRequestArgs(tt.domains, tt.method, tt.outputFullchainFile, tt.outputKeyFile, tt.dnsProvider, tt.ca, false) + + assert.Equal(t, tt.want.args, args) + assert.Equal(t, tt.want.err, err) + }) + } +} diff --git a/backend/internal/acme/errors.go b/backend/internal/acme/errors.go new file mode 100644 index 00000000..b929ce66 --- /dev/null +++ b/backend/internal/acme/errors.go @@ -0,0 +1,10 @@ +package acme + +import "errors" + +// All errors relating to Acme.sh use +var ( + ErrDNSNeedsDNSProvider = errors.New("RequestCert dns method requires a dns provider") + ErrHTTPHasDNSProvider = errors.New("RequestCert http method does not need a dns provider") + ErrMethodNotSupported = errors.New("RequestCert method not supported") +) diff --git a/backend/internal/api/context/context.go b/backend/internal/api/context/context.go new file mode 100644 index 00000000..f3bc957b --- /dev/null +++ b/backend/internal/api/context/context.go @@ -0,0 +1,25 @@ +package context + +var ( + // BodyCtxKey is the name of the Body value on the context + BodyCtxKey = &contextKey{"Body"} + // UserIDCtxKey is the name of the UserID value on the context + UserIDCtxKey = &contextKey{"UserID"} + // FiltersCtxKey is the name of the Filters value on the context + FiltersCtxKey = &contextKey{"Filters"} + // PrettyPrintCtxKey is the name of the pretty print context + PrettyPrintCtxKey = &contextKey{"Pretty"} + // ExpansionCtxKey is the name of the expansion context + ExpansionCtxKey = &contextKey{"Expansion"} +) + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "context value: " + k.name +} diff --git a/backend/internal/api/filters/helpers.go b/backend/internal/api/filters/helpers.go new file mode 100644 index 00000000..5f5d5238 --- /dev/null +++ b/backend/internal/api/filters/helpers.go @@ -0,0 +1,208 @@ +package filters + +import ( + "fmt" + "strings" +) + +// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware +func NewFilterSchema(fieldSchemas []string) string { + return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", ")) +} + +// BoolFieldSchema returns the Field Schema for a Boolean accepted value field +func BoolFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, boolModifiers, filterBool, filterBool) +} + +// IntFieldSchema returns the Field Schema for a Integer accepted value field +func IntFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// StringFieldSchema returns the Field Schema for a String accepted value field +func StringFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, stringModifiers, filterString, filterString) +} + +// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex +func RegexFieldSchema(fieldName string, regex string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "%s" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "%s" + } + } + ] + } + } + }`, fieldName, stringModifiers, regex, regex) +} + +// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format +func DateFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format +// 2020-03-01T10:30:00+10:00 +func DateTimeFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +const allModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$" +}` + +const boolModifiers = `{ + "type": "string", + "pattern": "^(equals|not)$" +}` + +const stringModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin)$" +}` + +const filterBool = `{ + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" +}` + +const filterString = `{ + "type": "string", + "minLength": 1 +}` + +const baseFilterSchema = `{ + "type": "array", + "items": { + "oneOf": [ + %s + ] + } +}` diff --git a/backend/internal/api/handler/auth.go b/backend/internal/api/handler/auth.go new file mode 100644 index 00000000..18f2a989 --- /dev/null +++ b/backend/internal/api/handler/auth.go @@ -0,0 +1,93 @@ +package handler + +import ( + "encoding/json" + "net/http" + "time" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/entity/auth" + "npm/internal/entity/user" + "npm/internal/errors" + "npm/internal/logger" +) + +type setAuthModel struct { + Type string `json:"type" db:"type"` + Secret string `json:"secret,omitempty" db:"secret"` + CurrentSecret string `json:"current_secret,omitempty"` +} + +// SetAuth sets a auth method. This can be used for "me" and `2` for example +// Route: POST /users/:userID/auth +func SetAuth() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newAuth setAuthModel + err := json.Unmarshal(bodyBytes, &newAuth) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + userID, isSelf, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + // Load user + thisUser, thisUserErr := user.GetByID(userID) + if thisUserErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil) + return + } + + if thisUser.IsSystem { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil) + return + } + + // Load existing auth for user + userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type) + if userAuthErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil) + return + } + + if isSelf { + // confirm that the current_secret given is valid for the one stored in the database + validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret) + if validateErr != nil { + logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error()) + // Sleep for 1 second to prevent brute force password guessing + time.Sleep(time.Second) + + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil) + return + } + } + + if newAuth.Type == auth.TypePassword { + err := userAuth.SetPassword(newAuth.Secret) + if err != nil { + logger.Error("SetPasswordError", err) + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } + + if err = userAuth.Save(); err != nil { + logger.Error("AuthSaveError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) + return + } + + userAuth.Secret = "" + + // todo: add to audit-log + + h.ResultResponseJSON(w, r, http.StatusOK, userAuth) + } +} diff --git a/backend/internal/api/handler/certificate_authorities.go b/backend/internal/api/handler/certificate_authorities.go new file mode 100644 index 00000000..c822cc06 --- /dev/null +++ b/backend/internal/api/handler/certificate_authorities.go @@ -0,0 +1,141 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + "npm/internal/acme" + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/certificateauthority" + "npm/internal/logger" +) + +// GetCertificateAuthorities will return a list of Certificate Authorities +// Route: GET /certificate-authorities +func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, certificates) + } + } +} + +// GetCertificateAuthority will return a single Certificate Authority +// Route: GET /certificate-authorities/{caID} +func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert) + } + } +} + +// CreateCertificateAuthority will create a Certificate Authority +// Route: POST /certificate-authorities +func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newCA certificateauthority.Model + err := json.Unmarshal(bodyBytes, &newCA) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newCA.Check(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newCA.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil) + return + } + + if err = acme.CreateAccountKey(&newCA); err != nil { + logger.Error("CreateAccountKeyError", err) + } + + h.ResultResponseJSON(w, r, http.StatusOK, newCA) + } +} + +// UpdateCertificateAuthority updates a ca +// Route: PUT /certificate-authorities/{caID} +func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + ca, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &ca) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = ca.Check(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = ca.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, ca) + } + } +} + +// DeleteCertificateAuthority deletes a ca +// Route: DELETE /certificate-authorities/{caID} +func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete()) + } + } +} diff --git a/backend/internal/api/handler/certificates.go b/backend/internal/api/handler/certificates.go new file mode 100644 index 00000000..8c48ddd1 --- /dev/null +++ b/backend/internal/api/handler/certificates.go @@ -0,0 +1,145 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/api/schema" + "npm/internal/entity/certificate" +) + +// GetCertificates will return a list of Certificates +// Route: GET /certificates +func GetCertificates() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, certificates) + } + } +} + +// GetCertificate will return a single Certificate +// Route: GET /certificates/{certificateID} +func GetCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert) + } + } +} + +// CreateCertificate will create a Certificate +// Route: POST /certificates +func CreateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newCertificate certificate.Model + err := json.Unmarshal(bodyBytes, &newCertificate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newCertificate.UserID = userID + + if err = newCertificate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newCertificate) + } +} + +// UpdateCertificate updates a cert +// Route: PUT /certificates/{certificateID} +func UpdateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificateObject, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + // This is a special endpoint, as it needs to verify the schema payload + // based on the certificate type, without being given a type in the payload. + // The middleware would normally handle this. + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes) + if jsonErr != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil) + return + } + + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return + } + + err := json.Unmarshal(bodyBytes, &certificateObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = certificateObject.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, certificateObject) + } + } +} + +// DeleteCertificate deletes a cert +// Route: DELETE /certificates/{certificateID} +func DeleteCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete()) + } + } +} diff --git a/backend/internal/api/handler/config.go b/backend/internal/api/handler/config.go new file mode 100644 index 00000000..811d7580 --- /dev/null +++ b/backend/internal/api/handler/config.go @@ -0,0 +1,15 @@ +package handler + +import ( + "net/http" + h "npm/internal/api/http" + "npm/internal/config" +) + +// Config returns the entire configuration, for debug purposes +// Route: GET /config +func Config() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration) + } +} diff --git a/backend/internal/api/handler/dns_providers.go b/backend/internal/api/handler/dns_providers.go new file mode 100644 index 00000000..6040b6bd --- /dev/null +++ b/backend/internal/api/handler/dns_providers.go @@ -0,0 +1,159 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/dnsproviders" + "npm/internal/entity/dnsprovider" +) + +// GetDNSProviders will return a list of DNS Providers +// Route: GET /dns-providers +func GetDNSProviders() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, items) + } + } +} + +// GetDNSProvider will return a single DNS Provider +// Route: GET /dns-providers/{providerID} +func GetDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// CreateDNSProvider will create a DNS Provider +// Route: POST /dns-providers +func CreateDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newItem dnsprovider.Model + err := json.Unmarshal(bodyBytes, &newItem) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newItem.UserID = userID + + if err = newItem.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newItem) + } +} + +// UpdateDNSProvider updates a provider +// Route: PUT /dns-providers/{providerID} +func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &item) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = item.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// DeleteDNSProvider removes a provider +// Route: DELETE /dns-providers/{providerID} +func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + } + } +} + +// GetAcmeshProviders will return a list of acme.sh providers +// Route: GET /dns-providers/acmesh +func GetAcmeshProviders() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + items := dnsproviders.List() + h.ResultResponseJSON(w, r, http.StatusOK, items) + } +} + +// GetAcmeshProvider will return a single acme.sh provider +// Route: GET /dns-providers/acmesh/{acmeshID} +func GetAcmeshProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var acmeshID string + var err error + if acmeshID, err = getURLParamString(r, "acmeshID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, getErr := dnsproviders.Get(acmeshID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, getErr.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} diff --git a/backend/internal/api/handler/health.go b/backend/internal/api/handler/health.go new file mode 100644 index 00000000..b4df7a09 --- /dev/null +++ b/backend/internal/api/handler/health.go @@ -0,0 +1,34 @@ +package handler + +import ( + "net/http" + "npm/internal/acme" + h "npm/internal/api/http" + "npm/internal/config" +) + +type healthCheckResponse struct { + Version string `json:"version"` + Commit string `json:"commit"` + AcmeShVersion string `json:"acme.sh"` + Healthy bool `json:"healthy"` + IsSetup bool `json:"setup"` + ErrorReporting bool `json:"error_reporting"` +} + +// Health returns the health of the api +// Route: GET /health +func Health() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + health := healthCheckResponse{ + Version: config.Version, + Commit: config.Commit, + Healthy: true, + IsSetup: config.IsSetup, + AcmeShVersion: acme.GetAcmeShVersion(), + ErrorReporting: config.ErrorReporting, + } + + h.ResultResponseJSON(w, r, http.StatusOK, health) + } +} diff --git a/backend/internal/api/handler/helpers.go b/backend/internal/api/handler/helpers.go new file mode 100644 index 00000000..3d6cf39a --- /dev/null +++ b/backend/internal/api/handler/helpers.go @@ -0,0 +1,175 @@ +package handler + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "npm/internal/api/context" + "npm/internal/model" + + "github.com/go-chi/chi" +) + +const defaultLimit = 10 + +func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) { + var pageInfo model.PageInfo + var err error + + pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r) + if err != nil { + return pageInfo, err + } + + pageInfo.Offset, pageInfo.Limit, err = getPagination(r) + if err != nil { + return pageInfo, err + } + + pageInfo.Sort = getSortParameter(r) + + return pageInfo, nil +} + +func getDateRanges(r *http.Request) (time.Time, time.Time, error) { + queryValues := r.URL.Query() + from := queryValues.Get("from") + fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default + to := queryValues.Get("to") + toDate := time.Now() + + if from != "" { + var fromErr error + fromDate, fromErr = time.Parse(time.RFC3339, from) + if fromErr != nil { + return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+")) + } + } + + if to != "" { + var toErr error + toDate, toErr = time.Parse(time.RFC3339, to) + if toErr != nil { + return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+")) + } + } + + return fromDate, toDate, nil +} + +func getSortParameter(r *http.Request) []model.Sort { + var sortFields []model.Sort + + queryValues := r.URL.Query() + sortString := queryValues.Get("sort") + if sortString == "" { + return sortFields + } + + // Split sort fields up in to slice + sorts := strings.Split(sortString, ",") + for _, sortItem := range sorts { + if strings.Contains(sortItem, ".") { + theseItems := strings.Split(sortItem, ".") + + switch strings.ToLower(theseItems[1]) { + case "desc": + fallthrough + case "descending": + theseItems[1] = "DESC" + default: + theseItems[1] = "ASC" + } + + sortFields = append(sortFields, model.Sort{ + Field: theseItems[0], + Direction: theseItems[1], + }) + } else { + sortFields = append(sortFields, model.Sort{ + Field: sortItem, + Direction: "ASC", + }) + } + } + + return sortFields +} + +func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) { + queryValues := r.URL.Query() + varValue := queryValues.Get(varName) + + if varValue == "" && required { + return 0, fmt.Errorf("%v was not supplied in the request", varName) + } else if varValue == "" { + return defaultValue, nil + } + + varInt, intErr := strconv.Atoi(varValue) + if intErr != nil { + return 0, fmt.Errorf("%v is not a valid number", varName) + } + + return varInt, nil +} + +func getURLParamInt(r *http.Request, varName string) (int, error) { + required := true + defaultValue := 0 + paramStr := chi.URLParam(r, varName) + var err error + var paramInt int + + if paramStr == "" && required { + return 0, fmt.Errorf("%v was not supplied in the request", varName) + } else if paramStr == "" { + return defaultValue, nil + } + + if paramInt, err = strconv.Atoi(paramStr); err != nil { + return 0, fmt.Errorf("%v is not a valid number", varName) + } + + return paramInt, nil +} + +func getURLParamString(r *http.Request, varName string) (string, error) { + required := true + defaultValue := "" + paramStr := chi.URLParam(r, varName) + + if paramStr == "" && required { + return "", fmt.Errorf("%v was not supplied in the request", varName) + } else if paramStr == "" { + return defaultValue, nil + } + + return paramStr, nil +} + +func getPagination(r *http.Request) (int, int, error) { + var err error + offset, err := getQueryVarInt(r, "offset", false, 0) + if err != nil { + return 0, 0, err + } + limit, err := getQueryVarInt(r, "limit", false, defaultLimit) + if err != nil { + return 0, 0, err + } + + return offset, limit, nil +} + +// getExpandFromContext returns the Expansion setting +func getExpandFromContext(r *http.Request) []string { + expand, ok := r.Context().Value(context.ExpansionCtxKey).([]string) + if !ok { + return nil + } + return expand +} diff --git a/backend/internal/api/handler/host_templates.go b/backend/internal/api/handler/host_templates.go new file mode 100644 index 00000000..04defd1c --- /dev/null +++ b/backend/internal/api/handler/host_templates.go @@ -0,0 +1,130 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/host" + "npm/internal/entity/hosttemplate" +) + +// GetHostTemplates will return a list of Host Templates +// Route: GET /host-templates +func GetHostTemplates() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hosts, err := hosttemplate.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hosts) + } + } +} + +// GetHostTemplate will return a single Host Template +// Route: GET /host-templates/{templateID} +func GetHostTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID int + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := hosttemplate.GetByID(templateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// CreateHostTemplate will create a Host Template +// Route: POST /host-templates +func CreateHostTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newHostTemplate hosttemplate.Model + err := json.Unmarshal(bodyBytes, &newHostTemplate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newHostTemplate.UserID = userID + + if err = newHostTemplate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host Template: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newHostTemplate) + } +} + +// UpdateHostTemplate updates a host template +// Route: PUT /host-templates/{templateID} +func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID int + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hostTemplate, err := hosttemplate.GetByID(templateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &hostTemplate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = hostTemplate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate) + } + } +} + +// DeleteHostTemplate removes a host template +// Route: DELETE /host-templates/{templateID} +func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID int + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hostTemplate, err := host.GetByID(templateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate.Delete()) + } + } +} diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go new file mode 100644 index 00000000..3364d673 --- /dev/null +++ b/backend/internal/api/handler/hosts.go @@ -0,0 +1,140 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/host" + "npm/internal/validator" +) + +// GetHosts will return a list of Hosts +// Route: GET /hosts +func GetHosts() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hosts) + } + } +} + +// GetHost will return a single Host +// Route: GET /hosts/{hostID} +func GetHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hostObject, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + // nolint: errcheck,gosec + hostObject.Expand(getExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, hostObject) + } + } +} + +// CreateHost will create a Host +// Route: POST /hosts +func CreateHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newHost host.Model + err := json.Unmarshal(bodyBytes, &newHost) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newHost.UserID = userID + + if err = validator.ValidateHost(newHost); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newHost.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newHost) + } +} + +// UpdateHost updates a host +// Route: PUT /hosts/{hostID} +func UpdateHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hostObject, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &hostObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = hostObject.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + // nolint: errcheck,gosec + hostObject.Expand(getExpandFromContext(r)) + + h.ResultResponseJSON(w, r, http.StatusOK, hostObject) + } + } +} + +// DeleteHost removes a host +// Route: DELETE /hosts/{hostID} +func DeleteHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host.Delete()) + } + } +} diff --git a/backend/internal/api/handler/not_allowed.go b/backend/internal/api/handler/not_allowed.go new file mode 100644 index 00000000..966debab --- /dev/null +++ b/backend/internal/api/handler/not_allowed.go @@ -0,0 +1,14 @@ +package handler + +import ( + "net/http" + + h "npm/internal/api/http" +) + +// NotAllowed is a json error handler for when method is not allowed +func NotAllowed() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil) + } +} diff --git a/backend/internal/api/handler/not_found.go b/backend/internal/api/handler/not_found.go new file mode 100644 index 00000000..07f40c71 --- /dev/null +++ b/backend/internal/api/handler/not_found.go @@ -0,0 +1,64 @@ +package handler + +import ( + "errors" + "io" + "io/fs" + "mime" + "net/http" + "path/filepath" + "strings" + + "npm/embed" + h "npm/internal/api/http" +) + +var ( + assetsSub fs.FS + errIsDir = errors.New("path is dir") +) + +// NotFound is a json error handler for 404's and method not allowed. +// It also serves the react frontend as embedded files in the golang binary. +func NotFound() func(http.ResponseWriter, *http.Request) { + assetsSub, _ = fs.Sub(embed.Assets, "assets") + + return func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimLeft(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + + err := tryRead(assetsSub, path, w) + if err == errIsDir { + err = tryRead(assetsSub, "index.html", w) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + } + } else if err == nil { + return + } + + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + } +} + +func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error { + f, err := folder.Open(requestedPath) + if err != nil { + return err + } + + // nolint: errcheck + defer f.Close() + + stat, _ := f.Stat() + if stat.IsDir() { + return errIsDir + } + + contentType := mime.TypeByExtension(filepath.Ext(requestedPath)) + w.Header().Set("Content-Type", contentType) + _, err = io.Copy(w, f) + return err +} diff --git a/backend/internal/api/handler/schema.go b/backend/internal/api/handler/schema.go new file mode 100644 index 00000000..2fb37b50 --- /dev/null +++ b/backend/internal/api/handler/schema.go @@ -0,0 +1,108 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io/fs" + "net/http" + "strings" + + "npm/embed" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/logger" + + jsref "github.com/jc21/jsref" + "github.com/jc21/jsref/provider" +) + +var ( + swaggerSchema []byte + apiDocsSub fs.FS +) + +// Schema simply reads the swagger schema from disk and returns is raw +// Route: GET /schema +func Schema() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(getSchema())) + } +} + +func getSchema() []byte { + if swaggerSchema == nil { + apiDocsSub, _ = fs.Sub(embed.APIDocFiles, "api_docs") + + // nolint:gosec + swaggerSchema, _ = fs.ReadFile(apiDocsSub, "api.swagger.json") + + // Replace {{VERSION}} with Config Version + swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version)) + + // Dereference the JSON Schema: + var schema interface{} + if err := json.Unmarshal(swaggerSchema, &schema); err != nil { + logger.Error("SwaggerUnmarshalError", err) + return nil + } + + provider := provider.NewIoFS(apiDocsSub, "") + resolver := jsref.New() + err := resolver.AddProvider(provider) + if err != nil { + logger.Error("SchemaProviderError", err) + } + + result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...) + if err != nil { + logger.Error("SwaggerResolveError", err) + } else { + var marshalErr error + swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ") + if marshalErr != nil { + logger.Error("SwaggerMarshalError", err) + } + } + // End dereference + + // Replace incoming schemas with those we actually use in code + swaggerSchema = replaceIncomingSchemas(swaggerSchema) + } + return swaggerSchema +} + +func replaceIncomingSchemas(swaggerSchema []byte) []byte { + str := string(swaggerSchema) + + // Remember to include the double quotes in the replacement! + str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth()) + str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken()) + + str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority()) + str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority()) + + str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate()) + str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate("")) + + str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting()) + str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting()) + + str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser()) + str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser()) + + str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost()) + str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost()) + + str = strings.ReplaceAll(str, `"{{schema.CreateHostTemplate}}"`, schema.CreateHostTemplate()) + str = strings.ReplaceAll(str, `"{{schema.UpdateHostTemplate}}"`, schema.UpdateHostTemplate()) + + str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream()) + str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream()) + + str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider()) + str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider()) + + return []byte(str) +} diff --git a/backend/internal/api/handler/settings.go b/backend/internal/api/handler/settings.go new file mode 100644 index 00000000..b0e1d7ac --- /dev/null +++ b/backend/internal/api/handler/settings.go @@ -0,0 +1,98 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/setting" + + "github.com/go-chi/chi" +) + +// GetSettings will return a list of Settings +// Route: GET /settings +func GetSettings() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, settings) + } + } +} + +// GetSetting will return a single Setting +// Route: GET /settings/{name} +func GetSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + sett, err := setting.GetByName(name) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, sett) + } + } +} + +// CreateSetting will create a Setting +// Route: POST /settings +func CreateSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newSetting setting.Model + err := json.Unmarshal(bodyBytes, &newSetting) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newSetting.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newSetting) + } +} + +// UpdateSetting updates a setting +// Route: PUT /settings/{name} +func UpdateSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + settingName := chi.URLParam(r, "name") + + setting, err := setting.GetByName(settingName) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &setting) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = setting.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, setting) + } + } +} diff --git a/backend/internal/api/handler/streams.go b/backend/internal/api/handler/streams.go new file mode 100644 index 00000000..17c668e4 --- /dev/null +++ b/backend/internal/api/handler/streams.go @@ -0,0 +1,129 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/stream" +) + +// GetStreams will return a list of Streams +// Route: GET /hosts/streams +func GetStreams() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hosts) + } + } +} + +// GetStream will return a single Streams +// Route: GET /hosts/streams/{hostID} +func GetStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// CreateStream will create a Stream +// Route: POST /hosts/steams +func CreateStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newHost stream.Model + err := json.Unmarshal(bodyBytes, &newHost) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newHost.UserID = userID + + if err = newHost.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newHost) + } +} + +// UpdateStream updates a stream +// Route: PUT /hosts/streams/{hostID} +func UpdateStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &host) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = host.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// DeleteStream removes a stream +// Route: DELETE /hosts/streams/{hostID} +func DeleteStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host.Delete()) + } + } +} diff --git a/backend/internal/api/handler/tokens.go b/backend/internal/api/handler/tokens.go new file mode 100644 index 00000000..2f317a92 --- /dev/null +++ b/backend/internal/api/handler/tokens.go @@ -0,0 +1,89 @@ +package handler + +import ( + "encoding/json" + "net/http" + h "npm/internal/api/http" + "npm/internal/errors" + "npm/internal/logger" + "time" + + c "npm/internal/api/context" + "npm/internal/entity/auth" + "npm/internal/entity/user" + njwt "npm/internal/jwt" +) + +// tokenPayload is the structure we expect from a incoming login request +type tokenPayload struct { + Type string `json:"type"` + Identity string `json:"identity"` + Secret string `json:"secret"` +} + +// NewToken Also known as a Login, requesting a new token with credentials +// Route: POST /tokens +func NewToken() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Read the bytes from the body + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var payload tokenPayload + err := json.Unmarshal(bodyBytes, &payload) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Find user + userObj, userErr := user.GetByEmail(payload.Identity) + if userErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + if userObj.IsDisabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) + return + } + + // Get Auth + authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type) + if authErr != nil { + logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error()) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + // Verify Auth + validateErr := authObj.ValidateSecret(payload.Secret) + if validateErr != nil { + logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error()) + // Sleep for 1 second to prevent brute force password guessing + time.Sleep(time.Second) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + if response, err := njwt.Generate(&userObj); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } + } +} + +// RefreshToken an existing token by given them a new one with the same claims +// Route: GET /tokens +func RefreshToken() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // TODO: Use your own methods to verify an existing user is + // able to refresh their token and then give them a new one + userObj, _ := user.GetByEmail("jc@jc21.com") + if response, err := njwt.Generate(&userObj); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } + } +} diff --git a/backend/internal/api/handler/users.go b/backend/internal/api/handler/users.go new file mode 100644 index 00000000..192ff7b2 --- /dev/null +++ b/backend/internal/api/handler/users.go @@ -0,0 +1,235 @@ +package handler + +import ( + "encoding/json" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/config" + "npm/internal/entity/auth" + "npm/internal/entity/user" + "npm/internal/errors" + "npm/internal/logger" + + "github.com/go-chi/chi" +) + +// GetUsers returns all users +// Route: GET /users +func GetUsers() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, users) + } + } +} + +// GetUser returns a specific user +// Route: GET /users/{userID} +func GetUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + userID, _, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + userObject, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + // nolint: errcheck,gosec + userObject.Expand(getExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, userObject) + } + } +} + +// UpdateUser updates a user +// Route: PUT /users/{userID} +func UpdateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + userID, self, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + userObject, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + // nolint: errcheck,gosec + userObject.Expand([]string{"capabilities"}) + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &userObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if userObject.IsDisabled && self { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil) + return + } + + if err = userObject.Save(); err != nil { + if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + logger.Error("UpdateUserError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + if !self { + err = userObject.SaveCapabilities() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + } + + // nolint: errcheck,gosec + userObject.Expand(getExpandFromContext(r)) + + h.ResultResponseJSON(w, r, http.StatusOK, userObject) + } + } +} + +// DeleteUser removes a user +// Route: DELETE /users/{userID} +func DeleteUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var userID int + var err error + if userID, err = getURLParamInt(r, "userID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + myUserID, _ := r.Context().Value(c.UserIDCtxKey).(int) + if myUserID == userID { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil) + return + } + + user, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, user.Delete()) + } + } +} + +// CreateUser creates a user +// Route: POST /users +func CreateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newUser user.Model + err := json.Unmarshal(bodyBytes, &newUser) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newUser.Save(); err != nil { + if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + logger.Error("UpdateUserError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + // Set the permissions to full-admin for this user + if !config.IsSetup { + newUser.Capabilities = []string{user.CapabilityFullAdmin} + } + + // nolint: errcheck,gosec + err = newUser.SaveCapabilities() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + + // newUser has been saved, now save their auth + if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 { + newUser.Auth.UserID = newUser.ID + if newUser.Auth.Type == auth.TypePassword { + err = newUser.Auth.SetPassword(newUser.Auth.Secret) + if err != nil { + logger.Error("SetPasswordError", err) + } + } + + if err = newUser.Auth.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) + return + } + + newUser.Auth.Secret = "" + } + + if !config.IsSetup { + config.IsSetup = true + logger.Info("A new user was created, leaving Setup Mode") + } + + h.ResultResponseJSON(w, r, http.StatusOK, newUser) + } +} + +// DeleteUsers is only available in debug mode for cypress tests +// Route: DELETE /users +func DeleteUsers() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + err := user.DeleteAll() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + // also change setup to true + config.IsSetup = false + logger.Info("Users have been wiped, entering Setup Mode") + h.ResultResponseJSON(w, r, http.StatusOK, true) + } + } +} + +func getUserIDFromRequest(r *http.Request) (int, bool, error) { + userIDstr := chi.URLParam(r, "userID") + selfUserID, _ := r.Context().Value(c.UserIDCtxKey).(int) + + var userID int + self := false + if userIDstr == "me" { + // Get user id from Token + userID = selfUserID + self = true + } else { + var userIDerr error + if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil { + return 0, false, userIDerr + } + self = selfUserID == userID + } + return userID, self, nil +} diff --git a/backend/internal/api/http/requests.go b/backend/internal/api/http/requests.go new file mode 100644 index 00000000..3e7103c0 --- /dev/null +++ b/backend/internal/api/http/requests.go @@ -0,0 +1,46 @@ +package http + +import ( + "context" + "encoding/json" + "errors" + + "github.com/qri-io/jsonschema" +) + +var ( + // ErrInvalidJSON is an error for invalid json + ErrInvalidJSON = errors.New("JSON is invalid") + // ErrInvalidPayload is an error for invalid incoming data + ErrInvalidPayload = errors.New("Payload is invalid") +) + +// ValidateRequestSchema takes a Schema and the Content to validate against it +func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) { + var jsonErrors []jsonschema.KeyError + var schemaBytes = []byte(schema) + + // Make sure the body is valid JSON + if !isJSON(requestBody) { + return jsonErrors, ErrInvalidJSON + } + + rs := &jsonschema.Schema{} + if err := json.Unmarshal(schemaBytes, rs); err != nil { + return jsonErrors, err + } + + var validationErr error + ctx := context.TODO() + if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 { + return jsonErrors, validationErr + } + + // Valid + return nil, nil +} + +func isJSON(bytes []byte) bool { + var js map[string]interface{} + return json.Unmarshal(bytes, &js) == nil +} diff --git a/backend/internal/api/http/responses.go b/backend/internal/api/http/responses.go new file mode 100644 index 00000000..c65736d3 --- /dev/null +++ b/backend/internal/api/http/responses.go @@ -0,0 +1,91 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + c "npm/internal/api/context" + "npm/internal/errors" + "npm/internal/logger" + + "github.com/qri-io/jsonschema" +) + +// Response interface for standard API results +type Response struct { + Result interface{} `json:"result"` + Error interface{} `json:"error,omitempty"` +} + +// ErrorResponse interface for errors returned via the API +type ErrorResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` + Invalid interface{} `json:"invalid,omitempty"` +} + +// ResultResponseJSON will write the result as json to the http output +func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + + var response Response + resultClass := fmt.Sprintf("%v", reflect.TypeOf(result)) + + if resultClass == "http.ErrorResponse" { + response = Response{ + Error: result, + } + } else { + response = Response{ + Result: result, + } + } + + var payload []byte + var err error + if getPrettyPrintFromContext(r) { + payload, err = json.MarshalIndent(response, "", " ") + } else { + payload, err = json.Marshal(response) + } + + if err != nil { + logger.Error("ResponseMarshalError", err) + } + + fmt.Fprint(w, string(payload)) +} + +// ResultSchemaErrorJSON will format the result as a standard error object and send it for output +func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonschema.KeyError) { + errorResponse := ErrorResponse{ + Code: http.StatusBadRequest, + Message: errors.ErrValidationFailed, + Invalid: errs, + } + + ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse) +} + +// ResultErrorJSON will format the result as a standard error object and send it for output +func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) { + errorResponse := ErrorResponse{ + Code: status, + Message: message, + Invalid: extended, + } + + ResultResponseJSON(w, r, status, errorResponse) +} + +// getPrettyPrintFromContext returns the PrettyPrint setting +func getPrettyPrintFromContext(r *http.Request) bool { + pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool) + if !ok { + return false + } + return pretty +} diff --git a/backend/internal/api/middleware/access_control.go b/backend/internal/api/middleware/access_control.go new file mode 100644 index 00000000..18bca31b --- /dev/null +++ b/backend/internal/api/middleware/access_control.go @@ -0,0 +1,13 @@ +package middleware + +import ( + "net/http" +) + +// AccessControl sets http headers for responses +func AccessControl(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 00000000..65249f0b --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,94 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/config" + "npm/internal/entity/user" + njwt "npm/internal/jwt" + "npm/internal/logger" + "npm/internal/util" + + "github.com/go-chi/jwtauth" +) + +// DecodeAuth decodes an auth header +func DecodeAuth() func(http.Handler) http.Handler { + privateKey, privateKeyParseErr := njwt.GetPrivateKey() + if privateKeyParseErr != nil && privateKey == nil { + logger.Error("PrivateKeyParseError", privateKeyParseErr) + } + + publicKey, publicKeyParseErr := njwt.GetPublicKey() + if publicKeyParseErr != nil && publicKey == nil { + logger.Error("PublicKeyParseError", publicKeyParseErr) + } + + tokenAuth := jwtauth.New("RS256", privateKey, publicKey) + return jwtauth.Verifier(tokenAuth) +} + +// Enforce is a authentication middleware to enforce access from the +// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised +// response for any unverified tokens and passes the good ones through. +func Enforce(permission string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if config.IsSetup { + token, claims, err := jwtauth.FromContext(ctx) + + if err != nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil) + return + } + + userID := int(claims["uid"].(float64)) + _, enabled := user.IsEnabled(userID) + if token == nil || !token.Valid || !enabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + // Check if permissions exist for this user + if permission != "" { + // Since the permission that we require is not on the token, we have to get it from the DB + // So we don't go crazy with hits, we will use a memory cache + cacheKey := fmt.Sprintf("userCapabilties.%v", userID) + cacheItem, found := AuthCache.Get(cacheKey) + + var userCapabilities []string + if found { + userCapabilities = cacheItem.([]string) + } else { + // Get from db and store it + userCapabilities, err = user.GetCapabilities(userID) + if err != nil { + AuthCacheSet(cacheKey, userCapabilities) + } + } + + // Now check that they have the permission in their admin capabilities + // full-admin can do anything + if !util.SliceContainsItem(userCapabilities, user.CapabilityFullAdmin) && !util.SliceContainsItem(userCapabilities, permission) { + // Access denied + logger.Debug("User has: %+v but needs %s", userCapabilities, permission) + h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil) + return + } + } + + // Add claims to context + ctx = context.WithValue(ctx, c.UserIDCtxKey, userID) + } + + // Token is authenticated, continue as normal + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/backend/internal/api/middleware/auth_cache.go b/backend/internal/api/middleware/auth_cache.go new file mode 100644 index 00000000..66becfe7 --- /dev/null +++ b/backend/internal/api/middleware/auth_cache.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "time" + + "npm/internal/logger" + + cache "github.com/patrickmn/go-cache" +) + +// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints +var AuthCache *cache.Cache + +// AuthCacheInit will create a new Memory Cache +func AuthCacheInit() { + logger.Debug("Creating a new AuthCache") + AuthCache = cache.New(1*time.Minute, 5*time.Minute) +} + +// AuthCacheSet will store the item in memory for the expiration time +func AuthCacheSet(k string, x interface{}) { + AuthCache.Set(k, x, cache.DefaultExpiration) +} diff --git a/backend/internal/api/middleware/body_context.go b/backend/internal/api/middleware/body_context.go new file mode 100644 index 00000000..68cfaa08 --- /dev/null +++ b/backend/internal/api/middleware/body_context.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "context" + "io/ioutil" + "net/http" + + c "npm/internal/api/context" +) + +// BodyContext simply adds the body data to a context item +func BodyContext() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Grab the Body Data + var body []byte + if r.Body != nil { + body, _ = ioutil.ReadAll(r.Body) + } + // Add it to the context + ctx := r.Context() + ctx = context.WithValue(ctx, c.BodyCtxKey, body) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/backend/internal/api/middleware/cors.go b/backend/internal/api/middleware/cors.go new file mode 100644 index 00000000..f2de6d72 --- /dev/null +++ b/backend/internal/api/middleware/cors.go @@ -0,0 +1,88 @@ +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi" +) + +var methodMap = []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodTrace, +} + +func getRouteMethods(routes chi.Router, path string) []string { + var methods []string + tctx := chi.NewRouteContext() + for _, method := range methodMap { + if routes.Match(tctx, method, path) { + methods = append(methods, method) + } + } + return methods +} + +var headersAllowedByCORS = []string{ + "Authorization", + "Host", + "Content-Type", + "Connection", + "User-Agent", + "Cache-Control", + "Accept-Encoding", + "X-Jumbo-AppKey", + "X-Jumbo-SKey", + "X-Jumbo-SV", + "X-Jumbo-Timestamp", + "X-Jumbo-Version", + "X-Jumbo-Customer-Id", +} + +// Cors handles cors headers +func Cors(routes chi.Router) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + methods := getRouteMethods(routes, r.URL.Path) + if len(methods) == 0 { + // no route no cors + next.ServeHTTP(w, r) + return + } + methods = append(methods, http.MethodOptions) + w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) + w.Header().Set("Access-Control-Allow-Headers", + strings.Join(headersAllowedByCORS, ","), + ) + next.ServeHTTP(w, r) + }) + } +} + +// Options handles options requests +func Options(routes chi.Router) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + methods := getRouteMethods(routes, r.URL.Path) + if len(methods) == 0 { + // no route shouldn't have options + next.ServeHTTP(w, r) + return + } + if r.Method == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, "{}") + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/enforce_setup.go b/backend/internal/api/middleware/enforce_setup.go new file mode 100644 index 00000000..3c75ccd9 --- /dev/null +++ b/backend/internal/api/middleware/enforce_setup.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "fmt" + "net/http" + + h "npm/internal/api/http" + "npm/internal/config" +) + +// EnforceSetup will error if the config setup doesn't match what is required +func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if config.IsSetup != shouldBeSetup { + state := "during" + if config.IsSetup { + state = "after" + } + h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/expansion.go b/backend/internal/api/middleware/expansion.go new file mode 100644 index 00000000..871bd27e --- /dev/null +++ b/backend/internal/api/middleware/expansion.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + c "npm/internal/api/context" +) + +// Expansion will determine whether the request should have objects expanded +// with ?expand=1 or ?expand=true +func Expansion(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expandStr := r.URL.Query().Get("expand") + if expandStr != "" { + ctx := r.Context() + ctx = context.WithValue(ctx, c.ExpansionCtxKey, strings.Split(expandStr, ",")) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + next.ServeHTTP(w, r) + } + }) +} diff --git a/backend/internal/api/middleware/filters.go b/backend/internal/api/middleware/filters.go new file mode 100644 index 00000000..885defef --- /dev/null +++ b/backend/internal/api/middleware/filters.go @@ -0,0 +1,115 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/model" + "npm/internal/util" + "strings" + + "github.com/qri-io/jsonschema" +) + +// Filters will accept a pre-defined schemaData to validate against the GET query params +// passed in to this endpoint. This will ensure that the filters are not injecting SQL. +// After we have determined what the Filters are to be, they are saved on the Context +// to be used later in other endpoints. +func Filters(schemaData string) func(http.Handler) http.Handler { + reservedFilterKeys := []string{ + "limit", + "offset", + "sort", + "order", + "expand", + "t", // This is used as a timestamp paramater in some clients and can be ignored + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var filters []model.Filter + for key, val := range r.URL.Query() { + key = strings.ToLower(key) + + // Split out the modifier from the field name and set a default modifier + var keyParts []string + keyParts = strings.Split(key, ":") + if len(keyParts) == 1 { + // Default modifier + keyParts = append(keyParts, "equals") + } + + // Only use this filter if it's not a reserved get param + if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) { + for _, valItem := range val { + // Check that the val isn't empty + if len(strings.TrimSpace(valItem)) > 0 { + valSlice := []string{valItem} + if keyParts[1] == "in" || keyParts[1] == "notin" { + valSlice = strings.Split(valItem, ",") + } + + filters = append(filters, model.Filter{ + Field: keyParts[0], + Modifier: keyParts[1], + Value: valSlice, + }) + } + } + } + } + + // Only validate schema if there are filters to validate + if len(filters) > 0 { + ctx := r.Context() + + // Marshal the Filters in to a JSON string so that the Schema Validation works against it + filterData, marshalErr := json.MarshalIndent(filters, "", " ") + if marshalErr != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil) + return + } + + // Create root schema + rs := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(schemaData), rs); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil) + return + } + + // Validate it + errors, jsonError := rs.ValidateBytes(ctx, filterData) + if jsonError != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil) + return + } + + if len(errors) > 0 { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors) + return + } + + ctx = context.WithValue(ctx, c.FiltersCtxKey, filters) + next.ServeHTTP(w, r.WithContext(ctx)) + + } else { + next.ServeHTTP(w, r) + } + }) + } +} + +// GetFiltersFromContext returns the Filters +func GetFiltersFromContext(r *http.Request) []model.Filter { + filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter) + if !ok { + // the assertion failed + var emptyFilters []model.Filter + return emptyFilters + } + return filters +} diff --git a/backend/internal/api/middleware/pretty_print.go b/backend/internal/api/middleware/pretty_print.go new file mode 100644 index 00000000..270d2a24 --- /dev/null +++ b/backend/internal/api/middleware/pretty_print.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "context" + "net/http" + + c "npm/internal/api/context" +) + +// PrettyPrint will determine whether the request should be pretty printed in output +// with ?pretty=1 or ?pretty=true +func PrettyPrint(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + prettyStr := r.URL.Query().Get("pretty") + if prettyStr == "1" || prettyStr == "true" { + ctx := r.Context() + ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + next.ServeHTTP(w, r) + } + }) +} diff --git a/backend/internal/api/middleware/schema.go b/backend/internal/api/middleware/schema.go new file mode 100644 index 00000000..3254e042 --- /dev/null +++ b/backend/internal/api/middleware/schema.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + + "github.com/qri-io/jsonschema" +) + +// CheckRequestSchema checks the payload against schema +func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) { + // Create root schema + rs := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(schemaData), rs); err != nil { + return nil, fmt.Errorf("Schema Fatal: %v", err) + } + + // Validate it + schemaErrors, jsonError := rs.ValidateBytes(ctx, payload) + if jsonError != nil { + return nil, jsonError + } + + return schemaErrors, nil +} + +// EnforceRequestSchema accepts a schema and validates the request body against it +func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Get content from context + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go new file mode 100644 index 00000000..27bc32bd --- /dev/null +++ b/backend/internal/api/router.go @@ -0,0 +1,198 @@ +package api + +import ( + "net/http" + "time" + + "npm/internal/api/handler" + "npm/internal/api/middleware" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/entity/certificate" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/entity/host" + "npm/internal/entity/hosttemplate" + "npm/internal/entity/setting" + "npm/internal/entity/stream" + "npm/internal/entity/user" + "npm/internal/logger" + + "github.com/go-chi/chi" + chiMiddleware "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" +) + +// NewRouter returns a new router object +func NewRouter() http.Handler { + // Cors + cors := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"}, + AllowCredentials: true, + MaxAge: 300, + }) + + r := chi.NewRouter() + r.Use( + middleware.AccessControl, + middleware.Cors(r), + middleware.Options(r), + cors.Handler, + chiMiddleware.RealIP, + chiMiddleware.Recoverer, + chiMiddleware.Throttle(5), + chiMiddleware.Timeout(30*time.Second), + middleware.PrettyPrint, + middleware.Expansion, + middleware.DecodeAuth(), + middleware.BodyContext(), + ) + + return applyRoutes(r) +} + +// applyRoutes is where the magic happens +func applyRoutes(r chi.Router) chi.Router { + middleware.AuthCacheInit() + r.NotFound(handler.NotFound()) + r.MethodNotAllowed(handler.NotAllowed()) + + // API + r.Route("/api", func(r chi.Router) { + r.Get("/", handler.Health()) + r.Get("/schema", handler.Schema()) + r.With(middleware.EnforceSetup(true), middleware.Enforce("")). + Get("/config", handler.Config()) + + // Tokens + r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) { + r.With(middleware.EnforceRequestSchema(schema.GetToken())). + Post("/", handler.NewToken()) + r.With(middleware.Enforce("")). + Get("/", handler.RefreshToken()) + }) + + // Users + r.Route("/users", func(r chi.Router) { + r.With(middleware.EnforceSetup(true), middleware.Enforce("")).Get("/{userID:(?:me)}", handler.GetUser()) + r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Get("/{userID:(?:[0-9]+)}", handler.GetUser()) + + r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser()) + r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).With(middleware.Filters(user.GetFilterSchema())). + Get("/", handler.GetUsers()) + r.With(middleware.EnforceRequestSchema(schema.CreateUser()), middleware.Enforce(user.CapabilityUsersManage)). + Post("/", handler.CreateUser()) + + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce("")). + Put("/{userID:(?:me)}", handler.UpdateUser()) + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce(user.CapabilityUsersManage)). + Put("/{userID:(?:[0-9]+)}", handler.UpdateUser()) + + // Auth + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce("")). + Post("/{userID:(?:me)}/auth", handler.SetAuth()) + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce(user.CapabilityUsersManage)). + Post("/{userID:(?:[0-9]+)}/auth", handler.SetAuth()) + }) + + // Only available in debug mode: delete users without auth + if config.GetLogLevel() == logger.DebugLevel { + r.Delete("/users", handler.DeleteUsers()) + } + + // Settings + r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) { + r.With(middleware.Filters(setting.GetFilterSchema())). + Get("/", handler.GetSettings()) + r.Get("/{name}", handler.GetSetting()) + r.With(middleware.EnforceRequestSchema(schema.CreateSetting())). + Post("/", handler.CreateSetting()) + r.With(middleware.EnforceRequestSchema(schema.UpdateSetting())). + Put("/{name}", handler.UpdateSetting()) + }) + + // DNS Providers + r.With(middleware.EnforceSetup(true)).Route("/dns-providers", func(r chi.Router) { + r.With(middleware.Filters(dnsprovider.GetFilterSchema()), middleware.Enforce(user.CapabilityDNSProvidersView)). + Get("/", handler.GetDNSProviders()) + r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Get("/{providerID:[0-9]+}", handler.GetDNSProvider()) + r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Delete("/{providerID:[0-9]+}", handler.DeleteDNSProvider()) + r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.CreateDNSProvider())). + Post("/", handler.CreateDNSProvider()) + r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())). + Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider()) + + r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) { + r.Get("/{acmeshID:[a-z0-9_]+}", handler.GetAcmeshProvider()) + r.Get("/", handler.GetAcmeshProviders()) + }) + }) + + // Certificate Authorities + r.With(middleware.EnforceSetup(true)).Route("/certificate-authorities", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView), middleware.Filters(certificateauthority.GetFilterSchema())). + Get("/", handler.GetCertificateAuthorities()) + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)).Get("/{caID:[0-9]+}", handler.GetCertificateAuthority()) + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority()) + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())). + Post("/", handler.CreateCertificateAuthority()) + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). + Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority()) + }) + + // Certificates + r.With(middleware.EnforceSetup(true)).Route("/certificates", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityCertificatesView), middleware.Filters(certificate.GetFilterSchema())). + Get("/", handler.GetCertificates()) + r.With(middleware.Enforce(user.CapabilityCertificatesView)).Get("/{certificateID:[0-9]+}", handler.GetCertificate()) + r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Delete("/{certificateID:[0-9]+}", handler.DeleteCertificate()) + r.With(middleware.Enforce(user.CapabilityCertificatesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificate())). + Post("/", handler.CreateCertificate()) + /* + r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())). + Put("/{certificateID:[0-9]+}", handler.UpdateCertificate()) + */ + r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Put("/{certificateID:[0-9]+}", handler.UpdateCertificate()) + }) + + // Hosts + r.With(middleware.EnforceSetup(true)).Route("/hosts", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(host.GetFilterSchema())). + Get("/", handler.GetHosts()) + r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{hostID:[0-9]+}", handler.GetHost()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteHost()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateHost())). + Post("/", handler.CreateHost()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.UpdateHost())). + Put("/{hostID:[0-9]+}", handler.UpdateHost()) + }) + + // Host Templates + r.With(middleware.EnforceSetup(true)).Route("/host-templates", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityHostTemplatesView), middleware.Filters(hosttemplate.GetFilterSchema())). + Get("/", handler.GetHostTemplates()) + r.With(middleware.Enforce(user.CapabilityHostTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetHostTemplates()) + r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteHostTemplate()) + r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateHostTemplate())). + Post("/", handler.CreateHostTemplate()) + r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateHostTemplate())). + Put("/{templateID:[0-9]+}", handler.UpdateHostTemplate()) + }) + + // Streams + r.With(middleware.EnforceSetup(true)).Route("/streams", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityStreamsView), middleware.Filters(stream.GetFilterSchema())). + Get("/", handler.GetStreams()) + r.With(middleware.Enforce(user.CapabilityStreamsView)).Get("/{hostID:[0-9]+}", handler.GetStream()) + r.With(middleware.Enforce(user.CapabilityStreamsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteStream()) + r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.CreateStream())). + Post("/", handler.CreateStream()) + r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())). + Put("/{hostID:[0-9]+}", handler.UpdateStream()) + }) + }) + + return r +} diff --git a/backend/internal/api/router_test.go b/backend/internal/api/router_test.go new file mode 100644 index 00000000..78ec784f --- /dev/null +++ b/backend/internal/api/router_test.go @@ -0,0 +1,44 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "npm/internal/config" + + "github.com/stretchr/testify/assert" +) + +var ( + r = NewRouter() + version = "3.0.0" + commit = "abcdefgh" + sentryDSN = "" +) + +// Tear up/down +func TestMain(m *testing.M) { + config.Init(&version, &commit, &sentryDSN) + code := m.Run() + os.Exit(code) +} + +func TestGetHealthz(t *testing.T) { + respRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/", nil) + + r.ServeHTTP(respRec, req) + assert.Equal(t, http.StatusOK, respRec.Code) + assert.Contains(t, respRec.Body.String(), "healthy") +} + +func TestNonExistent(t *testing.T) { + respRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil) + + r.ServeHTTP(respRec, req) + assert.Equal(t, http.StatusNotFound, respRec.Code) + assert.Equal(t, respRec.Body.String(), `{"result":null,"error":{"code":404,"message":"Not found"}}`, "404 Message should match") +} diff --git a/backend/internal/api/schema/certificates.go b/backend/internal/api/schema/certificates.go new file mode 100644 index 00000000..1326f6f1 --- /dev/null +++ b/backend/internal/api/schema/certificates.go @@ -0,0 +1,209 @@ +package schema + +import ( + "fmt" + + "npm/internal/entity/certificate" +) + +// This validation is strictly for Custom certificates +// and the combination of values that must be defined +func createCertificateCustom() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("custom"), stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for HTTP certificates +// and the combination of values that must be defined +func createCertificateHTTP() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "certificate_authority_id", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "certificate_authority_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + }, + "is_ecc": { + "type": "integer", + "minimum": 0, + "maximum": 1 + } + } + }`, strictString("http"), intMinOne, stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for DNS certificates +// and the combination of values that must be defined +func createCertificateDNS() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "certificate_authority_id", + "dns_provider_id", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "certificate_authority_id": %s, + "dns_provider_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + }, + "is_ecc": { + "type": "integer", + "minimum": 0, + "maximum": 1 + } + } + }`, strictString("dns"), intMinOne, intMinOne, stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for MKCERT certificates +// and the combination of values that must be defined +func createCertificateMkcert() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("mkcert"), stringMinMax(1, 100), domainNames()) +} + +func updateCertificateHTTP() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "certificate_authority_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, intMinOne, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateDNS() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "certificate_authority_id": %s, + "dns_provider_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, intMinOne, intMinOne, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateCustom() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateMkcert() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, stringMinMax(1, 100), domainNames()) +} + +// CreateCertificate is the schema for incoming data validation +func CreateCertificate() string { + return fmt.Sprintf(` + { + "oneOf": [%s, %s, %s, %s] + }`, createCertificateHTTP(), createCertificateDNS(), createCertificateCustom(), createCertificateMkcert()) +} + +// UpdateCertificate is the schema for incoming data validation +func UpdateCertificate(certificateType string) string { + switch certificateType { + case certificate.TypeHTTP: + return updateCertificateHTTP() + case certificate.TypeDNS: + return updateCertificateDNS() + case certificate.TypeCustom: + return updateCertificateCustom() + case certificate.TypeMkcert: + return updateCertificateMkcert() + default: + return fmt.Sprintf(` + { + "oneOf": [%s, %s, %s, %s] + }`, updateCertificateHTTP(), updateCertificateDNS(), updateCertificateCustom(), updateCertificateMkcert()) + } +} diff --git a/backend/internal/api/schema/common.go b/backend/internal/api/schema/common.go new file mode 100644 index 00000000..9313bcb6 --- /dev/null +++ b/backend/internal/api/schema/common.go @@ -0,0 +1,70 @@ +package schema + +import "fmt" + +func strictString(value string) string { + return fmt.Sprintf(`{ + "type": "string", + "pattern": "^%s$" + }`, value) +} + +const intMinOne = ` +{ + "type": "integer", + "minimum": 1 +} +` + +const boolean = ` +{ + "type": "boolean" +} +` + +func stringMinMax(minLength, maxLength int) string { + return fmt.Sprintf(`{ + "type": "string", + "minLength": %d, + "maxLength": %d + }`, minLength, maxLength) +} + +func capabilties() string { + return `{ + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }` +} + +func domainNames() string { + return fmt.Sprintf(` + { + "type": "array", + "minItems": 1, + "items": %s + }`, stringMinMax(4, 255)) +} + +const anyType = ` +{ + "anyOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "integer" + } + ] +} +` diff --git a/backend/internal/api/schema/create_certificate_authority.go b/backend/internal/api/schema/create_certificate_authority.go new file mode 100644 index 00000000..113c2ce3 --- /dev/null +++ b/backend/internal/api/schema/create_certificate_authority.go @@ -0,0 +1,25 @@ +package schema + +import "fmt" + +// CreateCertificateAuthority is the schema for incoming data validation +func CreateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "acmesh_server", + "max_domains" + ], + "properties": { + "name": %s, + "acmesh_server": %s, + "max_domains": %s, + "ca_bundle": %s, + "is_wildcard_supported": %s + } + } + `, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean) +} diff --git a/backend/internal/api/schema/create_dns_provider.go b/backend/internal/api/schema/create_dns_provider.go new file mode 100644 index 00000000..03b2000d --- /dev/null +++ b/backend/internal/api/schema/create_dns_provider.go @@ -0,0 +1,51 @@ +package schema + +import ( + "fmt" + "strings" + + "npm/internal/dnsproviders" + "npm/internal/util" +) + +// CreateDNSProvider is the schema for incoming data validation +func CreateDNSProvider() string { + allProviders := dnsproviders.GetAll() + fmtStr := fmt.Sprintf(`{"oneOf": [%s]}`, strings.TrimRight(strings.Repeat("\n%s,", len(allProviders)), ",")) + + allSchemasWrapped := make([]string, 0) + for providerName, provider := range allProviders { + allSchemasWrapped = append(allSchemasWrapped, createDNSProviderType(providerName, provider.Schema)) + } + + return fmt.Sprintf(fmtStr, util.ConvertStringSliceToInterface(allSchemasWrapped)...) +} + +func createDNSProviderType(name, metaSchema string) string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "acmesh_name", + "name", + "meta" + ], + "properties": { + "acmesh_name": { + "type": "string", + "pattern": "^%s$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "dns_sleep": { + "type": "integer" + }, + "meta": %s + } + } + `, name, metaSchema) +} diff --git a/backend/internal/api/schema/create_host.go b/backend/internal/api/schema/create_host.go new file mode 100644 index 00000000..0c448427 --- /dev/null +++ b/backend/internal/api/schema/create_host.go @@ -0,0 +1,80 @@ +package schema + +import "fmt" + +// CreateHost is the schema for incoming data validation +// This schema supports 3 possible types with different data combinations: +// - proxy +// - redirection +// - dead +func CreateHost() string { + return fmt.Sprintf(` + { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "domain_names", + "host_template_id" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^proxy$" + }, + "host_template_id": { + "type": "integer", + "minimum": 1 + }, + "listen_interface": %s, + "domain_names": %s, + "upstream_id": { + "type": "integer" + }, + "certificate_id": { + "type": "integer" + }, + "access_list_id": { + "type": "integer" + }, + "ssl_forced": { + "type": "boolean" + }, + "caching_enabled": { + "type": "boolean" + }, + "block_exploits": { + "type": "boolean" + }, + "allow_websocket_upgrade": { + "type": "boolean" + }, + "http2_support": { + "type": "boolean" + }, + "hsts_enabled": { + "type": "boolean" + }, + "hsts_subdomains": { + "type": "boolean" + }, + "paths": { + "type": "string" + }, + "upstream_options": { + "type": "string" + }, + "advanced_config": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + } + } + } + ] + } + `, stringMinMax(0, 255), domainNames()) +} diff --git a/backend/internal/api/schema/create_host_template.go b/backend/internal/api/schema/create_host_template.go new file mode 100644 index 00000000..ebb0cf42 --- /dev/null +++ b/backend/internal/api/schema/create_host_template.go @@ -0,0 +1,30 @@ +package schema + +// CreateHostTemplate is the schema for incoming data validation +func CreateHostTemplate() string { + return ` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "host_type", + "template" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "host_type": { + "type": "string", + "pattern": "^proxy|redirect|dead|stream$" + }, + "template": { + "type": "string", + "minLength": 20 + } + } + } + ` +} diff --git a/backend/internal/api/schema/create_setting.go b/backend/internal/api/schema/create_setting.go new file mode 100644 index 00000000..dca3869c --- /dev/null +++ b/backend/internal/api/schema/create_setting.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// CreateSetting is the schema for incoming data validation +func CreateSetting() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": %s, + "value": %s + } + } + `, stringMinMax(2, 100), anyType) +} diff --git a/backend/internal/api/schema/create_stream.go b/backend/internal/api/schema/create_stream.go new file mode 100644 index 00000000..792b8818 --- /dev/null +++ b/backend/internal/api/schema/create_stream.go @@ -0,0 +1,27 @@ +package schema + +import "fmt" + +// CreateStream is the schema for incoming data validation +func CreateStream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "name", + "domain_names" + ], + "properties": { + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/create_user.go b/backend/internal/api/schema/create_user.go new file mode 100644 index 00000000..ee17617a --- /dev/null +++ b/backend/internal/api/schema/create_user.go @@ -0,0 +1,42 @@ +package schema + +import "fmt" + +// CreateUser is the schema for incoming data validation +func CreateUser() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "email", + "is_disabled", + "capabilities" + ], + "properties": { + "name": %s, + "nickname": %s, + "email": %s, + "is_disabled": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": [ + "type", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^password$" + }, + "secret": %s + } + }, + "capabilities": %s + } + } + `, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties()) +} diff --git a/backend/internal/api/schema/get_token.go b/backend/internal/api/schema/get_token.go new file mode 100644 index 00000000..fe1a9502 --- /dev/null +++ b/backend/internal/api/schema/get_token.go @@ -0,0 +1,28 @@ +package schema + +import "fmt" + +// GetToken is the schema for incoming data validation +// nolint: gosec +func GetToken() string { + stdField := stringMinMax(1, 255) + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "identity", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^password$" + }, + "identity": %s, + "secret": %s + } + } + `, stdField, stdField) +} diff --git a/backend/internal/api/schema/set_auth.go b/backend/internal/api/schema/set_auth.go new file mode 100644 index 00000000..f2df26aa --- /dev/null +++ b/backend/internal/api/schema/set_auth.go @@ -0,0 +1,25 @@ +package schema + +import "fmt" + +// SetAuth is the schema for incoming data validation +func SetAuth() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^password$" + }, + "secret": %s, + "current_secret": %s + } + } + `, stringMinMax(8, 225), stringMinMax(8, 225)) +} diff --git a/backend/internal/api/schema/update_certificate_authority.go b/backend/internal/api/schema/update_certificate_authority.go new file mode 100644 index 00000000..e53db5c2 --- /dev/null +++ b/backend/internal/api/schema/update_certificate_authority.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// UpdateCertificateAuthority is the schema for incoming data validation +func UpdateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "acmesh_server": %s, + "max_domains": %s, + "ca_bundle": %s, + "is_wildcard_supported": %s + } + } + `, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean) +} diff --git a/backend/internal/api/schema/update_dns_provider.go b/backend/internal/api/schema/update_dns_provider.go new file mode 100644 index 00000000..b852c88a --- /dev/null +++ b/backend/internal/api/schema/update_dns_provider.go @@ -0,0 +1,20 @@ +package schema + +import "fmt" + +// UpdateDNSProvider is the schema for incoming data validation +func UpdateDNSProvider() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(1, 100)) +} diff --git a/backend/internal/api/schema/update_host.go b/backend/internal/api/schema/update_host.go new file mode 100644 index 00000000..78d1322f --- /dev/null +++ b/backend/internal/api/schema/update_host.go @@ -0,0 +1,27 @@ +package schema + +import "fmt" + +// UpdateHost is the schema for incoming data validation +func UpdateHost() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "host_template_id": { + "type": "integer", + "minimum": 1 + }, + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/update_host_template.go b/backend/internal/api/schema/update_host_template.go new file mode 100644 index 00000000..d51cf342 --- /dev/null +++ b/backend/internal/api/schema/update_host_template.go @@ -0,0 +1,22 @@ +package schema + +// UpdateHostTemplate is the schema for incoming data validation +func UpdateHostTemplate() string { + return ` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "template": { + "type": "string", + "minLength": 20 + } + } + } + ` +} diff --git a/backend/internal/api/schema/update_setting.go b/backend/internal/api/schema/update_setting.go new file mode 100644 index 00000000..e9af221b --- /dev/null +++ b/backend/internal/api/schema/update_setting.go @@ -0,0 +1,17 @@ +package schema + +import "fmt" + +// UpdateSetting is the schema for incoming data validation +func UpdateSetting() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "value": %s + } + } + `, anyType) +} diff --git a/backend/internal/api/schema/update_stream.go b/backend/internal/api/schema/update_stream.go new file mode 100644 index 00000000..51d85ff6 --- /dev/null +++ b/backend/internal/api/schema/update_stream.go @@ -0,0 +1,23 @@ +package schema + +import "fmt" + +// UpdateStream is the schema for incoming data validation +func UpdateStream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/update_user.go b/backend/internal/api/schema/update_user.go new file mode 100644 index 00000000..5eb4fd8a --- /dev/null +++ b/backend/internal/api/schema/update_user.go @@ -0,0 +1,23 @@ +package schema + +import "fmt" + +// UpdateUser is the schema for incoming data validation +func UpdateUser() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "nickname": %s, + "email": %s, + "is_disabled": { + "type": "boolean" + }, + "capabilities": %s + } + } + `, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties()) +} diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go new file mode 100644 index 00000000..c64de44a --- /dev/null +++ b/backend/internal/api/server.go @@ -0,0 +1,19 @@ +package api + +import ( + "fmt" + "net/http" + + "npm/internal/logger" +) + +const httpPort = 3000 + +// StartServer creates a http server +func StartServer() { + logger.Info("Server starting on port %v", httpPort) + err := http.ListenAndServe(fmt.Sprintf(":%v", httpPort), NewRouter()) + if err != nil { + logger.Error("HttpListenError", err) + } +} diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js deleted file mode 100644 index 422b4f46..00000000 --- a/backend/internal/audit-log.js +++ /dev/null @@ -1,78 +0,0 @@ -const error = require('../lib/error'); -const auditLogModel = require('../models/audit-log'); - -const internalAuditLog = { - - /** - * All logs - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('auditlog:list') - .then(() => { - let query = auditLogModel - .query() - .orderBy('created_on', 'DESC') - .orderBy('id', 'DESC') - .limit(100) - .allowEager('[user]'); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('meta', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * 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: (access, data) => { - return new Promise((resolve, reject) => { - // Default the user id - if (typeof data.user_id === 'undefined' || !data.user_id) { - data.user_id = access.token.getUserId(1); - } - - if (typeof data.action === 'undefined' || !data.action) { - reject(new error.InternalValidationError('Audit log entry must contain an Action')); - } else { - // Make sure at least 1 of the IDs are set and action - resolve(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 || {} - })); - } - }); - } -}; - -module.exports = internalAuditLog; diff --git a/backend/internal/cache/cache.go b/backend/internal/cache/cache.go new file mode 100644 index 00000000..347ce92f --- /dev/null +++ b/backend/internal/cache/cache.go @@ -0,0 +1,51 @@ +package cache + +import ( + "time" + + "npm/internal/entity/setting" + "npm/internal/logger" +) + +// Cache is a memory cache +type Cache struct { + Settings *map[string]setting.Model +} + +// Status is the status of last update +type Status struct { + LastUpdate time.Time + Valid bool +} + +// NewCache will create and return a new Cache object +func NewCache() *Cache { + return &Cache{ + Settings: nil, + } +} + +// Refresh will refresh all cache items +func (c *Cache) Refresh() { + c.RefreshSettings() +} + +// Clear will clear the cache +func (c *Cache) Clear() { + c.Settings = nil +} + +// RefreshSettings will refresh the settings from db +func (c *Cache) RefreshSettings() { + logger.Info("Cache refreshing Settings") + /* + c.ProductOffers = client.GetProductOffers() + + if c.ProductOffers != nil { + c.Status["product_offers"] = Status{ + LastUpdate: time.Now(), + Valid: true, + } + } + */ +} diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js deleted file mode 100644 index 7c8fddee..00000000 --- a/backend/internal/certificate.js +++ /dev/null @@ -1,1223 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const https = require('https'); -const tempWrite = require('temp-write'); -const moment = require('moment'); -const logger = require('../logger').ssl; -const error = require('../lib/error'); -const utils = require('../lib/utils'); -const certificateModel = require('../models/certificate'); -const dnsPlugins = require('../global/certbot-dns-plugins'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const internalHost = require('./host'); -const letsencryptStaging = process.env.NODE_ENV !== 'production'; -const letsencryptConfig = '/etc/letsencrypt.ini'; -const certbotCommand = 'certbot'; -const archiver = require('archiver'); -const path = require('path'); -const { isArray } = require('lodash'); - -function omissions() { - return ['is_deleted']; -} - -const internalCertificate = { - - allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], - intervalTimeout: 1000 * 60 * 60, // 1 hour - interval: null, - intervalProcessing: false, - - 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 close to expiry...'); - - const cmd = certbotCommand + ' renew --non-interactive --quiet ' + - '--config "' + letsencryptConfig + '" ' + - '--preferred-challenges "dns,http" ' + - '--disable-hook-validation ' + - (letsencryptStaging ? '--staging' : ''); - - return utils.exec(cmd) - .then((result) => { - if (result) { - logger.info('Renew Result: ' + result); - } - - return internalNginx.reload() - .then(() => { - logger.info('Renew Complete'); - return result; - }); - }) - .then(() => { - // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times - return certificateModel - .query() - .where('is_deleted', 0) - .andWhere('provider', 'letsencrypt') - .then((certificates) => { - if (certificates && certificates.length) { - let promises = []; - - certificates.map(function (certificate) { - promises.push( - internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .where('id', certificate.id) - .andWhere('provider', 'letsencrypt') - .patch({ - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }); - }) - .catch((err) => { - // Don't want to stop the train here, just log the error - logger.error(err.message); - }) - ); - }); - - return Promise.all(promises); - } - }); - }) - .then(() => { - internalCertificate.intervalProcessing = false; - }) - .catch((err) => { - logger.error(err); - internalCertificate.intervalProcessing = false; - }); - } - }, - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - return access.can('certificates:create', data) - .then(() => { - data.owner_user_id = access.token.getUserId(1); - - if (data.provider === 'letsencrypt') { - data.nice_name = data.domain_names.join(', '); - } - - return certificateModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((certificate) => { - 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 - return internalHost.getHostsWithDomains(certificate.domain_names) - .then((in_use_result) => { - // 2. Disable them in nginx temporarily - return internalCertificate.disableInUseHosts(in_use_result) - .then(() => { - return in_use_result; - }); - }) - .then((in_use_result) => { - // With DNS challenge no config is needed, so skip 3 and 5. - if (certificate.meta.dns_challenge) { - return internalNginx.reload().then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); - }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalCertificate.enableInUseHosts(in_use_result) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); - } else { - // 3. Generate the LE config - return internalNginx.generateLetsEncryptRequestConfig(certificate) - .then(internalNginx.reload) - .then(async() => await new Promise((r) => setTimeout(r, 5000))) - .then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSsl(certificate); - }) - .then(() => { - // 5. Remove LE config - return internalNginx.deleteLetsEncryptRequestConfig(certificate); - }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalNginx.deleteLetsEncryptRequestConfig(certificate) - .then(() => { - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); - } - }) - .then(() => { - // At this point, the letsencrypt cert should exist on disk. - // Lets get the expiry date from the file and update the row silently - return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }) - .then((saved_row) => { - // Add cert data for audit log - saved_row.meta = _.assign({}, saved_row.meta, { - letsencrypt_certificate: cert_info - }); - - return saved_row; - }); - }); - }).catch(async (error) => { - // Delete the certificate from the database if it was not created successfully - await certificateModel - .query() - .deleteById(certificate.id); - - throw error; - }); - } else { - return certificate; - } - }).then((certificate) => { - - data.meta = _.assign({}, data.meta || {}, certificate.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'certificate', - object_id: certificate.id, - meta: data - }) - .then(() => { - return certificate; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.email] - * @param {String} [data.name] - * @return {Promise} - */ - update: (access, data) => { - return access.can('certificates:update', data.id) - .then((/*access_data*/) => { - return internalCertificate.get(access, {id: data.id}); - }) - .then((row) => { - 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); - } - - return certificateModel - .query() - .omit(omissions()) - .patchAndFetchById(row.id, data) - .then((saved_row) => { - saved_row.meta = internalCertificate.cleanMeta(saved_row.meta); - data.meta = internalCertificate.cleanMeta(data.meta); - - // Add row.nice_name for custom certs - if (saved_row.provider === 'other') { - data.nice_name = saved_row.nice_name; - } - - // Add to audit log - return 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 - }) - .then(() => { - return _.omit(saved_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) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('certificates:get', data.id) - .then((access_data) => { - let query = certificateModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - download: (access, data) => { - return new Promise((resolve, reject) => { - access.can('certificates:get', data) - .then(() => { - return internalCertificate.get(access, data); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id; - - if (!fs.existsSync(zipDirectory)) { - throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists'); - } - - let 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; - internalCertificate.zipFiles(certFiles, opName) - .then(() => { - logger.debug('zip completed : ', opName); - const resp = { - fileName: opName - }; - resolve(resp); - }).catch((err) => reject(err)); - } else { - throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded'); - } - }).catch((err) => reject(err)); - }); - }, - - /** - * @param {String} source - * @param {String} out - * @returns {Promise} - */ - zipFiles(source, out) { - const archive = archiver('zip', { zlib: { level: 9 } }); - const stream = fs.createWriteStream(out); - - return new Promise((resolve, reject) => { - source - .map((fl) => { - let fileName = path.basename(fl); - logger.debug(fl, 'added to certificate zip'); - archive.file(fl, { name: fileName }); - }); - 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: (access, data) => { - return access.can('certificates:delete', data.id) - .then(() => { - return internalCertificate.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return certificateModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Add to audit log - row.meta = internalCertificate.cleanMeta(row.meta); - - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'certificate', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }) - .then(() => { - if (row.provider === 'letsencrypt') { - // Revoke the cert - return internalCertificate.revokeLetsEncryptSsl(row); - } - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Certs - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('certificates:list') - .then((access_data) => { - let query = certificateModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner]') - .orderBy('nice_name', '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') { - query.where(function () { - this.where('nice_name', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = certificateModel - .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 parseInt(row.count, 10); - }); - }, - - /** - * @param {Object} certificate - * @returns {Promise} - */ - writeCustomCert: (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, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }) - .then(() => { - return new Promise((resolve, reject) => { - fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Array} data.domain_names - * @param {String} data.meta.letsencrypt_email - * @param {Boolean} data.meta.letsencrypt_agree - * @returns {Promise} - */ - createQuickCertificate: (access, data) => { - return 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) => { - return new Promise((resolve) => { - // Put file contents into an object - let files = {}; - _.map(data.files, (file, name) => { - if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { - files[name] = file.data.toString(); - } - }); - - resolve(files); - }) - .then((files) => { - // For each file, create a temp file and write the contents to it - // Then test it depending on the file type - let 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: (access, data) => { - return internalCertificate.get(access, {id: data.id}) - .then((row) => { - if (row.provider !== 'other') { - throw new error.ValidationError('Cannot upload certificates for this type of provider'); - } - - return internalCertificate.validate(data) - .then((validations) => { - 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(); - } - }); - - // TODO: This uses a mysql only raw function that won't translate to postgres - return 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 - }) - .then((certificate) => { - console.log('ROWMETA:', row.meta); - certificate.meta = row.meta; - return internalCertificate.writeCustomCert(certificate); - }); - }) - .then(() => { - 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} private_key This is the entire key contents as a string - */ - checkPrivateKey: (private_key) => { - return tempWrite(private_key, '/tmp') - .then((filepath) => { - return new Promise((resolve, reject) => { - const failTimeout = setTimeout(() => { - reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.')); - }, 10000); - utils - .exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ') - .then((result) => { - clearTimeout(failTimeout); - if (!result.toLowerCase().includes('key is valid')) { - reject(new error.ValidationError('Result Validation Error: ' + result)); - } - fs.unlinkSync(filepath); - resolve(true); - }) - .catch((err) => { - clearTimeout(failTimeout); - fs.unlinkSync(filepath); - reject(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} [throw_expired] Throw when the certificate is out of date - */ - getCertificateInfo: (certificate, throw_expired) => { - return tempWrite(certificate, '/tmp') - .then((filepath) => { - return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired) - .then((certData) => { - 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} certificate_file The file location on disk - * @param {Boolean} [throw_expired] Throw when the certificate is out of date - */ - getCertificateInfoFromFile: (certificate_file, throw_expired) => { - let certData = {}; - - return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') - .then((result) => { - // subject=CN = something.example.com - const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; - const match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine subject from certificate: ' + result); - } - - certData['cn'] = match[1]; - }) - .then(() => { - return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); - }) - .then((result) => { - // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 - const regex = /^(?:issuer=)?(.*)$/gim; - const match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine issuer from certificate: ' + result); - } - - certData['issuer'] = match[1]; - }) - .then(() => { - return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); - }) - .then((result) => { - // notBefore=Jul 14 04:04:29 2018 GMT - // notAfter=Oct 12 04:04:29 2018 GMT - let validFrom = null; - let validTo = null; - - const lines = result.split('\n'); - lines.map(function (str) { - const regex = /^(\S+)=(.*)$/gim; - const match = regex.exec(str.trim()); - - if (match && typeof match[2] !== 'undefined') { - const date = 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; - } - } - }); - - if (!validFrom || !validTo) { - throw new error.ValidationError('Could not determine dates from certificate: ' + result); - } - - if (throw_expired && validTo < 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: function (meta, remove) { - internalCertificate.allowedSslFiles.map((key) => { - if (typeof meta[key] !== 'undefined' && meta[key]) { - if (remove) { - delete meta[key]; - } else { - meta[key] = true; - } - } - }); - - return meta; - }, - - /** - * Request a certificate using the http challenge - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - requestLetsEncryptSsl: (certificate) => { - logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const cmd = certbotCommand + ' certonly ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--authenticator webroot ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--preferred-challenges "dns,http" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - (letsencryptStaging ? '--staging' : ''); - - logger.info('Command:', cmd); - - return utils.exec(cmd) - .then((result) => { - logger.success(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`) - * @param {String | null} credentials the content of this providers credentials file - * @param {String} propagation_seconds the cloudflare api token - * @returns {Promise} - */ - requestLetsEncryptSslWithDnsChallenge: (certificate) => { - const dns_plugin = dnsPlugins[certificate.meta.dns_provider]; - - if (!dns_plugin) { - throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); - } - - logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - // Escape single quotes and backslashes - const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); - const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\''; - const prepareCmd = 'pip install ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies; - - // Whether the plugin has a ---credentials argument - const hasConfigArg = certificate.meta.dns_provider !== 'route53'; - - let mainCmd = certbotCommand + ' certonly ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - '--authenticator ' + dns_plugin.full_plugin_name + ' ' + - ( - hasConfigArg - ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' - : '' - ) + - ( - certificate.meta.propagation_seconds !== undefined - ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds - : '' - ) + - (letsencryptStaging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; - } - - logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`); - - return utils.exec(credentialsCmd) - .then(() => { - return utils.exec(prepareCmd) - .then(() => { - return utils.exec(mainCmd) - .then(async (result) => { - logger.info(result); - return result; - }); - }); - }).catch(async (err) => { - // Don't fail if file does not exist - const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`; - await utils.exec(delete_credentialsCmd); - throw err; - }); - }, - - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - renew: (access, data) => { - return access.can('certificates:update', data) - .then(() => { - return internalCertificate.get(access, data); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - const renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl; - - return renewMethod(certificate) - .then(() => { - return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); - }) - .then((cert_info) => { - return certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }); - }) - .then((updated_certificate) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'renewed', - object_type: 'certificate', - object_id: updated_certificate.id, - meta: updated_certificate - }) - .then(() => { - return updated_certificate; - }); - }); - } else { - throw new error.ValidationError('Only Let\'sEncrypt certificates can be renewed'); - } - }); - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - renewLetsEncryptSsl: (certificate) => { - logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const cmd = certbotCommand + ' renew --force-renewal ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--preferred-challenges "dns,http" ' + - '--no-random-sleep-on-renew ' + - '--disable-hook-validation ' + - (letsencryptStaging ? '--staging' : ''); - - logger.info('Command:', cmd); - - return utils.exec(cmd) - .then((result) => { - logger.info(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - renewLetsEncryptSslWithDnsChallenge: (certificate) => { - const dns_plugin = dnsPlugins[certificate.meta.dns_provider]; - - if (!dns_plugin) { - throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); - } - - logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - let mainCmd = certbotCommand + ' renew ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--disable-hook-validation ' + - '--no-random-sleep-on-renew ' + - (letsencryptStaging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; - } - - logger.info('Command:', mainCmd); - - return utils.exec(mainCmd) - .then(async (result) => { - logger.info(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - revokeLetsEncryptSsl: (certificate, throw_errors) => { - logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const mainCmd = certbotCommand + ' revoke ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + - '--delete-after-revoke ' + - (letsencryptStaging ? '--staging' : ''); - - // Don't fail command if file does not exist - const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; - - logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); - - return utils.exec(mainCmd) - .then(async (result) => { - await utils.exec(delete_credentialsCmd); - logger.info(result); - return result; - }) - .catch((err) => { - logger.error(err.message); - - if (throw_errors) { - throw err; - } - }); - }, - - /** - * @param {Object} certificate - * @returns {Boolean} - */ - hasLetsEncryptSslCerts: (certificate) => { - const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id; - - return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem'); - }, - - /** - * @param {Object} in_use_result - * @param {Number} in_use_result.total_count - * @param {Array} in_use_result.proxy_hosts - * @param {Array} in_use_result.redirection_hosts - * @param {Array} in_use_result.dead_hosts - */ - disableInUseHosts: (in_use_result) => { - if (in_use_result.total_count) { - let promises = []; - - if (in_use_result.proxy_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts)); - } - - if (in_use_result.redirection_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('redirection_host', in_use_result.redirection_hosts)); - } - - if (in_use_result.dead_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('dead_host', in_use_result.dead_hosts)); - } - - return Promise.all(promises); - - } else { - return Promise.resolve(); - } - }, - - /** - * @param {Object} in_use_result - * @param {Number} in_use_result.total_count - * @param {Array} in_use_result.proxy_hosts - * @param {Array} in_use_result.redirection_hosts - * @param {Array} in_use_result.dead_hosts - */ - enableInUseHosts: (in_use_result) => { - if (in_use_result.total_count) { - let promises = []; - - if (in_use_result.proxy_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts)); - } - - if (in_use_result.redirection_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('redirection_host', in_use_result.redirection_hosts)); - } - - if (in_use_result.dead_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('dead_host', in_use_result.dead_hosts)); - } - - return Promise.all(promises); - - } else { - return Promise.resolve(); - } - }, - - testHttpsChallenge: async (access, domains) => { - await access.can('certificates:list'); - - if (!isArray(domains)) { - throw new error.InternalValidationError('Domains must be an array of strings'); - } - if (domains.length === 0) { - throw new error.InternalValidationError('No domains provided'); - } - - // 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'}); - - async function performTestForDomain (domain) { - logger.info('Testing http challenge for ' + domain); - 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: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(formBody) - } - }; - - const result = await new Promise((resolve) => { - - const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) { - let responseBody = ''; - - res.on('data', (chunk) => responseBody = responseBody + chunk); - res.on('end', function () { - const parsedBody = JSON.parse(responseBody + ''); - if (res.statusCode !== 200) { - logger.warn(`Failed to test HTTP challenge for domain ${domain}`, res); - resolve(undefined); - } - resolve(parsedBody); - }); - }); - - // Make sure to write the request body. - req.write(formBody); - req.end(); - req.on('error', function (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'; - } else if (`${result.responsecode}` === '200' && result.htmlresponse === 'Success') { - // Server exists and has responded with the correct data - return 'ok'; - } else 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'; - } else 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'; - } else 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'; - } else { - // Other errors - logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); - return `other:${result.responsecode}`; - } - } - - const results = {}; - - for (const domain of domains){ - results[domain] = await performTestForDomain(domain); - } - - // Remove the test challenge file - fs.unlinkSync(testChallengeFile); - - return results; - } -}; - -module.exports = internalCertificate; diff --git a/backend/internal/config/args.go b/backend/internal/config/args.go new file mode 100644 index 00000000..6bade68d --- /dev/null +++ b/backend/internal/config/args.go @@ -0,0 +1,28 @@ +package config + +import ( + "fmt" + "os" + + "github.com/alexflint/go-arg" +) + +// ArgConfig is the settings for passing arguments to the command +type ArgConfig struct { + Version bool `arg:"-v" help:"print version and exit"` +} + +var ( + appArguments ArgConfig +) + +// InitArgs will parse arg vars +func InitArgs(version, commit *string) { + // nolint: errcheck, gosec + arg.MustParse(&appArguments) + + if appArguments.Version { + fmt.Printf("v%s (%s)\n", *version, *commit) + os.Exit(0) + } +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 00000000..d3d5944f --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,79 @@ +package config + +import ( + "fmt" + golog "log" + "runtime" + + "npm/internal/logger" + + "github.com/getsentry/sentry-go" + "github.com/vrischmann/envconfig" +) + +// Init will parse environment variables into the Env struct +func Init(version, commit, sentryDSN *string) { + // ErrorReporting is enabled until we load the status of it from the DB later + ErrorReporting = true + + Version = *version + Commit = *commit + + if err := envconfig.InitWithPrefix(&Configuration, "NPM"); err != nil { + fmt.Printf("%+v\n", err) + } + + initLogger(*sentryDSN) + logger.Info("Build Version: %s (%s)", Version, Commit) + createDataFolders() + loadKeys() +} + +// Init initialises the Log object and return it +func initLogger(sentryDSN string) { + // this removes timestamp prefixes from logs + golog.SetFlags(0) + + switch Configuration.Log.Level { + case "debug": + logLevel = logger.DebugLevel + case "warn": + logLevel = logger.WarnLevel + case "error": + logLevel = logger.ErrorLevel + default: + logLevel = logger.InfoLevel + } + + err := logger.Configure(&logger.Config{ + LogThreshold: logLevel, + Formatter: Configuration.Log.Format, + SentryConfig: sentry.ClientOptions{ + // This is the jc21 NginxProxyManager Sentry project, + // errors will be reported here (if error reporting is enable) + // and this project is private. No personal information should + // be sent in any error messages, only stacktraces. + Dsn: sentryDSN, + Release: Commit, + Dist: Version, + Environment: fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), + }, + }) + + if err != nil { + logger.Error("LoggerConfigurationError", err) + } +} + +// GetLogLevel returns the logger const level +func GetLogLevel() logger.Level { + return logLevel +} + +func isError(errorClass string, err error) bool { + if err != nil { + logger.Error(errorClass, err) + return true + } + return false +} diff --git a/backend/internal/config/folders.go b/backend/internal/config/folders.go new file mode 100644 index 00000000..bf3d43cf --- /dev/null +++ b/backend/internal/config/folders.go @@ -0,0 +1,34 @@ +package config + +import ( + "fmt" + "npm/internal/logger" + "os" +) + +// createDataFolders will recursively create these folders within the +// data folder defined in configuration. +func createDataFolders() { + folders := []string{ + "access", + "certificates", + "logs", + // Acme.sh: + Configuration.Acmesh.GetWellknown(), + // Nginx: + "nginx/hosts", + "nginx/streams", + "nginx/temp", + } + + for _, folder := range folders { + path := folder + if path[0:1] != "/" { + path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder) + } + logger.Debug("Creating folder: %s", path) + if err := os.MkdirAll(path, os.ModePerm); err != nil { + logger.Error("CreateDataFolderError", err) + } + } +} diff --git a/backend/internal/config/keys.go b/backend/internal/config/keys.go new file mode 100644 index 00000000..9ac3e903 --- /dev/null +++ b/backend/internal/config/keys.go @@ -0,0 +1,112 @@ +package config + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + + "npm/internal/logger" +) + +var keysFolder string +var publicKeyFile string +var privateKeyFile string + +func loadKeys() { + // check if keys folder exists in data folder + keysFolder = fmt.Sprintf("%s/keys", Configuration.DataFolder) + publicKeyFile = fmt.Sprintf("%s/public.key", keysFolder) + privateKeyFile = fmt.Sprintf("%s/private.key", keysFolder) + + if _, err := os.Stat(keysFolder); os.IsNotExist(err) { + // nolint:errcheck,gosec + os.Mkdir(keysFolder, 0700) + } + + // check if keys exist on disk + _, publicKeyErr := os.Stat(publicKeyFile) + _, privateKeyErr := os.Stat(privateKeyFile) + + // generate keys if either one doesn't exist + if os.IsNotExist(publicKeyErr) || os.IsNotExist(privateKeyErr) { + generateKeys() + saveKeys() + } + + // Load keys from disk + // nolint:gosec + publicKeyBytes, publicKeyBytesErr := ioutil.ReadFile(publicKeyFile) + // nolint:gosec + privateKeyBytes, privateKeyBytesErr := ioutil.ReadFile(privateKeyFile) + PublicKey = string(publicKeyBytes) + PrivateKey = string(privateKeyBytes) + + if isError("PublicKeyReadError", publicKeyBytesErr) || isError("PrivateKeyReadError", privateKeyBytesErr) || PublicKey == "" || PrivateKey == "" { + logger.Warn("There was an error loading keys, proceeding to generate new RSA keys") + generateKeys() + saveKeys() + } +} + +func generateKeys() { + reader := rand.Reader + bitSize := 4096 + + key, err := rsa.GenerateKey(reader, bitSize) + if isError("RSAGenerateError", err) { + return + } + + privateKey := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + privateKeyBuffer := new(bytes.Buffer) + err = pem.Encode(privateKeyBuffer, privateKey) + if isError("PrivatePEMEncodeError", err) { + return + } + + asn1Bytes, err2 := asn1.Marshal(key.PublicKey) + if isError("RSAMarshalError", err2) { + return + } + + publicKey := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: asn1Bytes, + } + + publicKeyBuffer := new(bytes.Buffer) + err = pem.Encode(publicKeyBuffer, publicKey) + if isError("PublicPEMEncodeError", err) { + return + } + + PublicKey = publicKeyBuffer.String() + PrivateKey = privateKeyBuffer.String() + logger.Info("Generated new RSA keys") +} + +func saveKeys() { + err := ioutil.WriteFile(publicKeyFile, []byte(PublicKey), 0600) + if err != nil { + logger.Error("PublicKeyWriteError", err) + } else { + logger.Info("Saved Public Key: %s", publicKeyFile) + } + + err = ioutil.WriteFile(privateKeyFile, []byte(PrivateKey), 0600) + if err != nil { + logger.Error("PrivateKeyWriteError", err) + } else { + logger.Info("Saved Private Key: %s", privateKeyFile) + } +} diff --git a/backend/internal/config/vars.go b/backend/internal/config/vars.go new file mode 100644 index 00000000..38207355 --- /dev/null +++ b/backend/internal/config/vars.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "npm/internal/logger" +) + +// Version is the version set by ldflags +var Version string + +// Commit is the git commit set by ldflags +var Commit string + +// IsSetup defines whether we have an admin user or not +var IsSetup bool + +// ErrorReporting defines whether we will send errors to Sentry +var ErrorReporting bool + +// PublicKey is the public key +var PublicKey string + +// PrivateKey is the private key +var PrivateKey string + +var logLevel logger.Level + +type log struct { + Level string `json:"level" envconfig:"optional,default=info"` + Format string `json:"format" envconfig:"optional,default=nice"` +} + +type acmesh struct { + Home string `json:"home" envconfig:"optional,default=/data/.acme.sh"` + ConfigHome string `json:"config_home" envconfig:"optional,default=/data/.acme.sh/config"` + CertHome string `json:"cert_home" envconfig:"optional,default=/data/.acme.sh/certs"` +} + +// Configuration is the main configuration object +var Configuration struct { + DataFolder string `json:"data_folder" envconfig:"optional,default=/data"` + Acmesh acmesh `json:"acmesh"` + Log log `json:"log"` +} + +// GetWellknown returns the well known path +func (a *acmesh) GetWellknown() string { + return fmt.Sprintf("%s/.well-known", a.Home) +} diff --git a/backend/internal/database/helpers.go b/backend/internal/database/helpers.go new file mode 100644 index 00000000..7553af3b --- /dev/null +++ b/backend/internal/database/helpers.go @@ -0,0 +1,46 @@ +package database + +import ( + "fmt" + "strings" + + "npm/internal/errors" + "npm/internal/model" + "npm/internal/util" +) + +const ( + // DateFormat for DateFormat + DateFormat = "2006-01-02" + // DateTimeFormat for DateTimeFormat + DateTimeFormat = "2006-01-02T15:04:05" +) + +// GetByQuery returns a row given a query, populating the model given +func GetByQuery(model interface{}, query string, params []interface{}) error { + db := GetInstance() + if db != nil { + err := db.Get(model, query, params...) + return err + } + + return errors.ErrDatabaseUnavailable +} + +// BuildOrderBySQL takes a `Sort` slice and constructs a query fragment +func BuildOrderBySQL(columns []string, sort *[]model.Sort) (string, []model.Sort) { + var sortStrings []string + var newSort []model.Sort + for _, sortItem := range *sort { + if util.SliceContainsItem(columns, sortItem.Field) { + sortStrings = append(sortStrings, fmt.Sprintf("`%s` %s", sortItem.Field, sortItem.Direction)) + newSort = append(newSort, sortItem) + } + } + + if len(sortStrings) > 0 { + return fmt.Sprintf("ORDER BY %s", strings.Join(sortStrings, ", ")), newSort + } + + return "", newSort +} diff --git a/backend/internal/database/migrator.go b/backend/internal/database/migrator.go new file mode 100644 index 00000000..8b81c408 --- /dev/null +++ b/backend/internal/database/migrator.go @@ -0,0 +1,202 @@ +package database + +import ( + "database/sql" + "fmt" + "io/fs" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "npm/embed" + "npm/internal/logger" + "npm/internal/util" + + "github.com/jmoiron/sqlx" +) + +// MigrationConfiguration options for the migrator. +type MigrationConfiguration struct { + Table string `json:"table"` + mux sync.Mutex +} + +// Default migrator configuration +var mConfiguration = MigrationConfiguration{ + Table: "migration", +} + +// ConfigureMigrator and will return error if missing required fields. +func ConfigureMigrator(c *MigrationConfiguration) error { + // ensure updates to the config are atomic + mConfiguration.mux.Lock() + defer mConfiguration.mux.Unlock() + if c == nil { + return fmt.Errorf("a non nil Configuration is mandatory") + } + if strings.TrimSpace(c.Table) != "" { + mConfiguration.Table = c.Table + } + mConfiguration.Table = c.Table + return nil +} + +type afterMigrationComplete func() + +// Migrate will perform the migration from start to finish +func Migrate(followup afterMigrationComplete) bool { + logger.Info("Migration: Started") + + // Try to connect to the database sleeping for 15 seconds in between + var db *sqlx.DB + for { + db = GetInstance() + if db == nil { + logger.Warn("Database is unavailable for migration, retrying in 15 seconds") + time.Sleep(15 * time.Second) + } else { + break + } + } + + // Check for migration table existence + if !tableExists(db, mConfiguration.Table) { + err := createMigrationTable(db) + if err != nil { + logger.Error("MigratorError", err) + return false + } + logger.Info("Migration: Migration Table created") + } + + // DO MIGRATION + migrationCount, migrateErr := performFileMigrations(db) + if migrateErr != nil { + logger.Error("MigratorError", migrateErr) + } + + if migrateErr == nil { + logger.Info("Migration: Completed %v migration files", migrationCount) + followup() + return true + } + return false +} + +// createMigrationTable performs a query to create the migration table +// with the name specified in the configuration +func createMigrationTable(db *sqlx.DB) error { + logger.Info("Migration: Creating Migration Table: %v", mConfiguration.Table) + // nolint:lll + query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%v` (filename TEXT PRIMARY KEY, migrated_on INTEGER NOT NULL DEFAULT 0)", mConfiguration.Table) + _, err := db.Exec(query) + return err +} + +// tableExists will check the database for the existence of the specified table. +func tableExists(db *sqlx.DB, tableName string) bool { + query := `SELECT CASE name WHEN $1 THEN true ELSE false END AS found FROM sqlite_master WHERE type='table' AND name = $1` + + row := db.QueryRowx(query, tableName) + if row == nil { + logger.Error("MigratorError", fmt.Errorf("Cannot check if table exists, no row returned: %v", tableName)) + return false + } + + var exists *bool + if err := row.Scan(&exists); err != nil { + if err == sql.ErrNoRows { + return false + } + logger.Error("MigratorError", err) + return false + } + return *exists +} + +// performFileMigrations will perform the actual migration, +// importing files and updating the database with the rows imported. +func performFileMigrations(db *sqlx.DB) (int, error) { + var importedCount = 0 + + // Grab a list of previously ran migrations from the database: + previousMigrations, prevErr := getPreviousMigrations(db) + if prevErr != nil { + return importedCount, prevErr + } + + // List up the ".sql" files on disk + err := fs.WalkDir(embed.MigrationFiles, ".", func(file string, d fs.DirEntry, err error) error { + if !d.IsDir() { + shortFile := filepath.Base(file) + + // Check if this file already exists in the previous migrations + // and if so, ignore it + if util.SliceContainsItem(previousMigrations, shortFile) { + return nil + } + + logger.Info("Migration: Importing %v", shortFile) + + sqlContents, ioErr := embed.MigrationFiles.ReadFile(path.Clean(file)) + if ioErr != nil { + return ioErr + } + + sqlString := string(sqlContents) + + tx := db.MustBegin() + if _, execErr := tx.Exec(sqlString); execErr != nil { + return execErr + } + if commitErr := tx.Commit(); commitErr != nil { + return commitErr + } + if markErr := markMigrationSuccessful(db, shortFile); markErr != nil { + return markErr + } + + importedCount++ + } + return nil + }) + + return importedCount, err +} + +// getPreviousMigrations will query the migration table for names +// of migrations we can ignore because they should have already +// been imported +func getPreviousMigrations(db *sqlx.DB) ([]string, error) { + var existingMigrations []string + // nolint:gosec + query := fmt.Sprintf("SELECT filename FROM `%v` ORDER BY filename", mConfiguration.Table) + rows, err := db.Queryx(query) + if err != nil { + if err == sql.ErrNoRows { + return existingMigrations, nil + } + return existingMigrations, err + } + + for rows.Next() { + var filename *string + err := rows.Scan(&filename) + if err != nil { + return existingMigrations, err + } + existingMigrations = append(existingMigrations, *filename) + } + + return existingMigrations, nil +} + +// markMigrationSuccessful will add a row to the migration table +func markMigrationSuccessful(db *sqlx.DB, filename string) error { + // nolint:gosec + query := fmt.Sprintf("INSERT INTO `%v` (filename) VALUES ($1)", mConfiguration.Table) + _, err := db.Exec(query, filename) + return err +} diff --git a/backend/internal/database/setup.go b/backend/internal/database/setup.go new file mode 100644 index 00000000..b4101db5 --- /dev/null +++ b/backend/internal/database/setup.go @@ -0,0 +1,37 @@ +package database + +import ( + "database/sql" + + "npm/internal/config" + "npm/internal/errors" + "npm/internal/logger" +) + +// CheckSetup Quick check by counting the number of users in the database +func CheckSetup() { + query := `SELECT COUNT(*) FROM "user" WHERE is_deleted = $1 AND is_disabled = $1 AND is_system = $1` + db := GetInstance() + + if db != nil { + row := db.QueryRowx(query, false) + var totalRows int + queryErr := row.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("SetupError", queryErr) + return + } + if totalRows == 0 { + logger.Warn("No users found, starting in Setup Mode") + } else { + config.IsSetup = true + logger.Info("Application is setup") + } + + if config.ErrorReporting { + logger.Warn("Error reporting is enabled - Application Errors WILL be sent to Sentry, you can disable this in the Settings interface") + } + } else { + logger.Error("DatabaseError", errors.ErrDatabaseUnavailable) + } +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go new file mode 100644 index 00000000..ebfa75f6 --- /dev/null +++ b/backend/internal/database/sqlite.go @@ -0,0 +1,74 @@ +package database + +import ( + "fmt" + "os" + + "npm/internal/config" + "npm/internal/logger" + + "github.com/jmoiron/sqlx" + + // Blank import for Sqlite + _ "github.com/mattn/go-sqlite3" +) + +var dbInstance *sqlx.DB + +// NewDB creates a new connection +func NewDB() { + logger.Info("Creating new DB instance") + db := SqliteDB() + if db != nil { + dbInstance = db + } +} + +// GetInstance returns an existing or new instance +func GetInstance() *sqlx.DB { + if dbInstance == nil { + NewDB() + } else if err := dbInstance.Ping(); err != nil { + NewDB() + } + + return dbInstance +} + +// SqliteDB Create sqlite client +func SqliteDB() *sqlx.DB { + dbFile := fmt.Sprintf("%s/nginxproxymanager.db", config.Configuration.DataFolder) + autocreate(dbFile) + db, err := sqlx.Open("sqlite3", dbFile) + if err != nil { + logger.Error("SqliteError", err) + return nil + } + + return db +} + +// Commit will close and reopen the db file +func Commit() *sqlx.DB { + if dbInstance != nil { + err := dbInstance.Close() + if err != nil { + logger.Error("DatabaseCloseError", err) + } + } + NewDB() + return dbInstance +} + +func autocreate(dbFile string) { + if _, err := os.Stat(dbFile); os.IsNotExist(err) { + // Create it + logger.Info("Creating Sqlite DB: %s", dbFile) + // nolint: gosec + _, err = os.Create(dbFile) + if err != nil { + logger.Error("FileCreateError", err) + } + Commit() + } +} diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js deleted file mode 100644 index d35fec25..00000000 --- a/backend/internal/dead-host.js +++ /dev/null @@ -1,461 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const deadHostModel = require('../models/dead_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalDeadHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('dead_hosts:create', data) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return deadHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalDeadHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // re-fetch with cert - return internalDeadHost.get(access, { - id: row.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row) - .then(() => { - return row; - }); - }) - .then((row) => { - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'dead-host', - 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 create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('dead_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return deadHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'dead-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalDeadHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row) - .then((new_meta) => { - row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(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) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('dead_hosts:get', data.id) - .then((access_data) => { - let query = deadHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('dead_hosts:delete', data.id) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('dead_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'dead-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('dead_hosts:update', data.id) - .then(() => { - return internalDeadHost.get(access, { - id: data.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); - } - - row.enabled = 1; - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 1 - }) - .then(() => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'enabled', - object_type: 'dead-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('dead_hosts:update', data.id) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); - } - - row.enabled = 0; - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 0 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('dead_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'disabled', - object_type: 'dead-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('dead_hosts:list') - .then((access_data) => { - let query = deadHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,certificate]') - .orderBy('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') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .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) => { - let query = deadHostModel - .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 parseInt(row.count, 10); - }); - } -}; - -module.exports = internalDeadHost; diff --git a/backend/internal/dnsproviders/common.go b/backend/internal/dnsproviders/common.go new file mode 100644 index 00000000..5b155efe --- /dev/null +++ b/backend/internal/dnsproviders/common.go @@ -0,0 +1,135 @@ +package dnsproviders + +import ( + "errors" + "npm/internal/util" +) + +type providerField struct { + Name string `json:"name"` + Type string `json:"type"` + IsRequired bool `json:"is_required"` + IsSecret bool `json:"is_secret"` + MetaKey string `json:"meta_key"` + EnvKey string `json:"-"` // not exposed in api +} + +// Provider is a simple struct +type Provider struct { + AcmeshName string `json:"acmesh_name"` + Schema string `json:"-"` + Fields []providerField `json:"fields"` +} + +// GetAcmeEnvVars will map the meta given to the env var required for +// acme.sh to use this dns provider +func (p *Provider) GetAcmeEnvVars(meta interface{}) map[string]string { + res := make(map[string]string) + for _, field := range p.Fields { + if acmeShEnvValue, found := util.FindItemInInterface(field.MetaKey, meta); found { + res[field.EnvKey] = acmeShEnvValue.(string) + } + } + return res +} + +// List returns an array of providers +func List() []Provider { + return []Provider{ + getDNSAd(), + getDNSAli(), + getDNSAws(), + getDNSCf(), + getDNSCloudns(), + getDNSCx(), + getDNSCyon(), + getDNSDgon(), + getDNSDNSimple(), + getDNSDp(), + getDNSDuckDNS(), + getDNSDyn(), + getDNSDynu(), + getDNSFreeDNS(), + getDNSGandiLiveDNS(), + getDNSGd(), + getDNSHe(), + getDNSInfoblox(), + getDNSIspconfig(), + getDNSLinodeV4(), + getDNSLua(), + getDNSMe(), + getDNSNamecom(), + getDNSOne(), + getDNSPDNS(), + getDNSUnoeuro(), + getDNSVscale(), + getDNSYandex(), + } +} + +// GetAll returns all the configured providers +func GetAll() map[string]Provider { + mp := make(map[string]Provider) + items := List() + for _, item := range items { + mp[item.AcmeshName] = item + } + return mp +} + +// Get returns a single provider by name +func Get(provider string) (Provider, error) { + all := GetAll() + if item, found := all[provider]; found { + return item, nil + } + return Provider{}, errors.New("provider_not_found") +} + +// GetAllSchemas returns a flat array with just the schemas +func GetAllSchemas() []string { + items := List() + mp := make([]string, 0) + for _, item := range items { + mp = append(mp, item.Schema) + } + return mp +} + +const commonKeySchema = ` +{ + "type": "object", + "required": [ + "api_key" + ], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + } +} +` + +// nolint: gosec +const commonKeySecretSchema = ` +{ + "type": "object", + "required": [ + "api_key", + "secret" + ], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + }, + "secret": { + "type": "string", + "minLength": 1 + } + } +} +` diff --git a/backend/internal/dnsproviders/dns_ad.go b/backend/internal/dnsproviders/dns_ad.go new file mode 100644 index 00000000..f3c5e588 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ad.go @@ -0,0 +1,17 @@ +package dnsproviders + +func getDNSAd() Provider { + return Provider{ + AcmeshName: "dns_ad", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "API Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "AD_API_KEY", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_ali.go b/backend/internal/dnsproviders/dns_ali.go new file mode 100644 index 00000000..d4906dfc --- /dev/null +++ b/backend/internal/dnsproviders/dns_ali.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSAli() Provider { + return Provider{ + AcmeshName: "dns_ali", + Schema: commonKeySecretSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "Ali_Key", + IsRequired: true, + }, + { + Name: "Secret", + Type: "password", + MetaKey: "secret", + EnvKey: "Ali_Secret", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_aws.go b/backend/internal/dnsproviders/dns_aws.go new file mode 100644 index 00000000..88139e88 --- /dev/null +++ b/backend/internal/dnsproviders/dns_aws.go @@ -0,0 +1,56 @@ +package dnsproviders + +const route53Schema = ` +{ + "type": "object", + "required": [ + "access_key_id", + "access_key" + ], + "additionalProperties": false, + "properties": { + "access_key_id": { + "type": "string", + "minLength": 10 + }, + "access_key": { + "type": "string", + "minLength": 10 + }, + "slow_rate": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSAws() Provider { + return Provider{ + AcmeshName: "dns_aws", + Schema: route53Schema, + Fields: []providerField{ + { + Name: "Access Key ID", + Type: "text", + MetaKey: "access_key_id", + EnvKey: "AWS_ACCESS_KEY_ID", + IsRequired: true, + }, + { + Name: "Secret Access Key", + Type: "password", + MetaKey: "access_key", + EnvKey: "AWS_SECRET_ACCESS_KEY", + IsRequired: true, + IsSecret: true, + }, + { + Name: "Slow Rate", + Type: "number", + MetaKey: "slow_rate", + EnvKey: "AWS_DNS_SLOWRATE", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cf.go b/backend/internal/dnsproviders/dns_cf.go new file mode 100644 index 00000000..dab20548 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cf.go @@ -0,0 +1,80 @@ +package dnsproviders + +const cloudflareSchema = ` +{ + "type": "object", + "required": [ + "api_key", + "email", + "token", + "account_id" + ], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + }, + "email": { + "type": "string", + "minLength": 5 + }, + "token": { + "type": "string", + "minLength": 5 + }, + "account_id": { + "type": "string", + "minLength": 1 + }, + "zone_id": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSCf() Provider { + return Provider{ + AcmeshName: "dns_cf", + Schema: cloudflareSchema, + Fields: []providerField{ + { + Name: "API Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "CF_Key", + IsRequired: true, + }, + { + Name: "Email", + Type: "text", + MetaKey: "email", + EnvKey: "CF_Email", + IsRequired: true, + }, + { + Name: "Token", + Type: "text", + MetaKey: "token", + EnvKey: "CF_Token", + IsRequired: true, + IsSecret: true, + }, + { + Name: "Account ID", + Type: "text", + MetaKey: "account_id", + EnvKey: "CF_Account_ID", + IsRequired: true, + }, + { + Name: "Zone ID", + Type: "string", + MetaKey: "zone_id", + EnvKey: "CF_Zone_ID", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cloudns.go b/backend/internal/dnsproviders/dns_cloudns.go new file mode 100644 index 00000000..7848a3a3 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cloudns.go @@ -0,0 +1,54 @@ +package dnsproviders + +const clouDNSNetSchema = ` +{ + "type": "object", + "required": [ + "password" + ], + "additionalProperties": false, + "properties": { + "auth_id": { + "type": "string", + "minLength": 1 + }, + "sub_auth_id": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSCloudns() Provider { + return Provider{ + AcmeshName: "dns_cloudns", + Schema: clouDNSNetSchema, + Fields: []providerField{ + { + Name: "Auth ID", + Type: "text", + MetaKey: "auth_id", + EnvKey: "CLOUDNS_AUTH_ID", + }, + { + Name: "Sub Auth ID", + Type: "text", + MetaKey: "sub_auth_id", + EnvKey: "CLOUDNS_SUB_AUTH_ID", + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "CLOUDNS_AUTH_PASSWORD", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cx.go b/backend/internal/dnsproviders/dns_cx.go new file mode 100644 index 00000000..39d23256 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cx.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSCx() Provider { + return Provider{ + AcmeshName: "dns_cx", + Schema: commonKeySecretSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "CX_Key", + IsRequired: true, + }, + { + Name: "Secret", + Type: "password", + MetaKey: "secret", + EnvKey: "CX_Secret", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cyon.go b/backend/internal/dnsproviders/dns_cyon.go new file mode 100644 index 00000000..cacd25d2 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cyon.go @@ -0,0 +1,57 @@ +package dnsproviders + +const cyonChSchema = ` +{ + "type": "object", + "required": [ + "user", + "password" + ], + "additionalProperties": false, + "properties": { + "user": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + }, + "otp_secret": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSCyon() Provider { + return Provider{ + AcmeshName: "dns_cyon", + Schema: cyonChSchema, + Fields: []providerField{ + { + Name: "User", + Type: "text", + MetaKey: "user", + EnvKey: "CY_Username", + IsRequired: true, + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "CY_Password", + IsRequired: true, + IsSecret: true, + }, + { + Name: "OTP Secret", + Type: "password", + MetaKey: "otp_secret", + EnvKey: "CY_OTP_Secret", + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dgon.go b/backend/internal/dnsproviders/dns_dgon.go new file mode 100644 index 00000000..0d8aaa1e --- /dev/null +++ b/backend/internal/dnsproviders/dns_dgon.go @@ -0,0 +1,18 @@ +package dnsproviders + +func getDNSDgon() Provider { + return Provider{ + AcmeshName: "dns_dgon", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "API Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "DO_API_KEY", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dnsimple.go b/backend/internal/dnsproviders/dns_dnsimple.go new file mode 100644 index 00000000..7fb7e983 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dnsimple.go @@ -0,0 +1,18 @@ +package dnsproviders + +func getDNSDNSimple() Provider { + return Provider{ + AcmeshName: "dns_dnsimple", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "OAuth Token", + Type: "text", + MetaKey: "api_key", + EnvKey: "DNSimple_OAUTH_TOKEN", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dp.go b/backend/internal/dnsproviders/dns_dp.go new file mode 100644 index 00000000..0c0579f9 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dp.go @@ -0,0 +1,46 @@ +package dnsproviders + +const dnsPodCnSchema = ` +{ + "type": "object", + "required": [ + "id", + "api_key" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "api_key": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSDp() Provider { + return Provider{ + AcmeshName: "dns_dp", + Schema: dnsPodCnSchema, + Fields: []providerField{ + { + Name: "ID", + Type: "text", + MetaKey: "id", + EnvKey: "DP_Id", + IsRequired: true, + }, + { + Name: "Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "DP_Key", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_duckdns.go b/backend/internal/dnsproviders/dns_duckdns.go new file mode 100644 index 00000000..f88e3cbe --- /dev/null +++ b/backend/internal/dnsproviders/dns_duckdns.go @@ -0,0 +1,18 @@ +package dnsproviders + +func getDNSDuckDNS() Provider { + return Provider{ + AcmeshName: "dns_duckdns", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "Token", + Type: "password", + MetaKey: "api_key", + EnvKey: "DuckDNS_Token", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dyn.go b/backend/internal/dnsproviders/dns_dyn.go new file mode 100644 index 00000000..a3b75fe8 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dyn.go @@ -0,0 +1,58 @@ +package dnsproviders + +const dynSchema = ` +{ + "type": "object", + "required": [ + "customer", + "username", + "password" + ], + "additionalProperties": false, + "properties": { + "customer": { + "type": "string", + "minLength": 1 + }, + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSDyn() Provider { + return Provider{ + AcmeshName: "dns_dyn", + Schema: dynSchema, + Fields: []providerField{ + { + Name: "Customer", + Type: "text", + MetaKey: "customer", + EnvKey: "DYN_Customer", + IsRequired: true, + }, + { + Name: "Username", + Type: "text", + MetaKey: "username", + EnvKey: "DYN_Username", + IsRequired: true, + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "DYN_Password", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dynu.go b/backend/internal/dnsproviders/dns_dynu.go new file mode 100644 index 00000000..468b7f8b --- /dev/null +++ b/backend/internal/dnsproviders/dns_dynu.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSDynu() Provider { + return Provider{ + AcmeshName: "dns_dynu", + Schema: commonKeySecretSchema, + Fields: []providerField{ + { + Name: "Client ID", + Type: "text", + MetaKey: "api_key", + EnvKey: "Dynu_ClientId", + IsRequired: true, + }, + { + Name: "Secret", + Type: "password", + MetaKey: "secret", + EnvKey: "Dynu_Secret", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_freedns.go b/backend/internal/dnsproviders/dns_freedns.go new file mode 100644 index 00000000..3c56069d --- /dev/null +++ b/backend/internal/dnsproviders/dns_freedns.go @@ -0,0 +1,46 @@ +package dnsproviders + +const freeDNSSchema = ` +{ + "type": "object", + "required": [ + "user", + "password" + ], + "additionalProperties": false, + "properties": { + "user": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSFreeDNS() Provider { + return Provider{ + AcmeshName: "dns_freedns", + Schema: freeDNSSchema, + Fields: []providerField{ + { + Name: "User", + Type: "text", + MetaKey: "user", + EnvKey: "FREEDNS_User", + IsRequired: true, + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "FREEDNS_Password", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_gandi_livedns.go b/backend/internal/dnsproviders/dns_gandi_livedns.go new file mode 100644 index 00000000..214b0619 --- /dev/null +++ b/backend/internal/dnsproviders/dns_gandi_livedns.go @@ -0,0 +1,17 @@ +package dnsproviders + +func getDNSGandiLiveDNS() Provider { + return Provider{ + AcmeshName: "dns_gandi_livedns", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "GANDI_LIVEDNS_KEY", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_gd.go b/backend/internal/dnsproviders/dns_gd.go new file mode 100644 index 00000000..0429c73b --- /dev/null +++ b/backend/internal/dnsproviders/dns_gd.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSGd() Provider { + return Provider{ + AcmeshName: "dns_gd", + Schema: commonKeySecretSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "GD_Key", + IsRequired: true, + }, + { + Name: "Secret", + Type: "password", + MetaKey: "secret", + EnvKey: "GD_Secret", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_he.go b/backend/internal/dnsproviders/dns_he.go new file mode 100644 index 00000000..dc6911e2 --- /dev/null +++ b/backend/internal/dnsproviders/dns_he.go @@ -0,0 +1,47 @@ +package dnsproviders + +// nolint: gosec +const commonUserPassSchema = ` +{ + "type": "object", + "required": [ + "username", + "password" + ], + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSHe() Provider { + return Provider{ + AcmeshName: "dns_he", + Schema: commonUserPassSchema, + Fields: []providerField{ + { + Name: "Username", + Type: "text", + MetaKey: "username", + EnvKey: "HE_Username", + IsRequired: true, + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "HE_Password", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_infoblox.go b/backend/internal/dnsproviders/dns_infoblox.go new file mode 100644 index 00000000..97368ca3 --- /dev/null +++ b/backend/internal/dnsproviders/dns_infoblox.go @@ -0,0 +1,46 @@ +package dnsproviders + +const infobloxSchema = ` +{ + "type": "object", + "required": [ + "credentials", + "server" + ], + "additionalProperties": false, + "properties": { + "credentials": { + "type": "string", + "minLength": 1 + }, + "server": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSInfoblox() Provider { + return Provider{ + AcmeshName: "dns_infoblox", + Schema: infobloxSchema, + Fields: []providerField{ + { + Name: "Credentials", + Type: "text", + MetaKey: "credentials", + EnvKey: "Infoblox_Creds", + IsRequired: true, + IsSecret: true, + }, + { + Name: "Server", + Type: "text", + MetaKey: "server", + EnvKey: "Infoblox_Server", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_ispconfig.go b/backend/internal/dnsproviders/dns_ispconfig.go new file mode 100644 index 00000000..00c1dd1a --- /dev/null +++ b/backend/internal/dnsproviders/dns_ispconfig.go @@ -0,0 +1,67 @@ +package dnsproviders + +const ispConfigSchema = ` +{ + "type": "object", + "required": [ + "user", + "password", + "api_url" + ], + "additionalProperties": false, + "properties": { + "user": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + }, + "api_url": { + "type": "string", + "minLength": 1 + }, + "insecure": { + "type": "string" + } + } +} +` + +func getDNSIspconfig() Provider { + return Provider{ + AcmeshName: "dns_ispconfig", + Schema: ispConfigSchema, + Fields: []providerField{ + { + Name: "User", + Type: "text", + MetaKey: "user", + EnvKey: "ISPC_User", + IsRequired: true, + }, + { + Name: "Password", + Type: "password", + MetaKey: "password", + EnvKey: "ISPC_Password", + IsRequired: true, + IsSecret: true, + }, + { + Name: "API URL", + Type: "text", + MetaKey: "api_url", + EnvKey: "ISPC_Api", + IsRequired: true, + }, + { + Name: "Insecure", + Type: "bool", + MetaKey: "insecure", + EnvKey: "ISPC_Api_Insecure", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_linode_v4.go b/backend/internal/dnsproviders/dns_linode_v4.go new file mode 100644 index 00000000..b79ff532 --- /dev/null +++ b/backend/internal/dnsproviders/dns_linode_v4.go @@ -0,0 +1,20 @@ +package dnsproviders + +// Note: https://github.com/acmesh-official/acme.sh/wiki/dnsapi#14-use-linode-domain-api +// needs 15 minute sleep, not currently implemented +func getDNSLinodeV4() Provider { + return Provider{ + AcmeshName: "dns_linode_v4", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "API Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "LINODE_V4_API_KEY", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_lua.go b/backend/internal/dnsproviders/dns_lua.go new file mode 100644 index 00000000..6c24a456 --- /dev/null +++ b/backend/internal/dnsproviders/dns_lua.go @@ -0,0 +1,46 @@ +package dnsproviders + +const luaDNSSchema = ` +{ + "type": "object", + "required": [ + "api_key", + "email" + ], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + }, + "email": { + "type": "string", + "minLength": 5 + } + } +} +` + +func getDNSLua() Provider { + return Provider{ + AcmeshName: "dns_lua", + Schema: luaDNSSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "LUA_Key", + IsRequired: true, + IsSecret: true, + }, + { + Name: "Email", + Type: "text", + MetaKey: "email", + EnvKey: "LUA_Email", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_me.go b/backend/internal/dnsproviders/dns_me.go new file mode 100644 index 00000000..bf888ca5 --- /dev/null +++ b/backend/internal/dnsproviders/dns_me.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSMe() Provider { + return Provider{ + AcmeshName: "dns_me", + Schema: commonKeySecretSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "text", + MetaKey: "api_key", + EnvKey: "ME_Key", + IsRequired: true, + }, + { + Name: "Secret", + Type: "password", + MetaKey: "secret", + EnvKey: "ME_Secret", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_namecom.go b/backend/internal/dnsproviders/dns_namecom.go new file mode 100644 index 00000000..5d040d92 --- /dev/null +++ b/backend/internal/dnsproviders/dns_namecom.go @@ -0,0 +1,46 @@ +package dnsproviders + +const nameComSchema = ` +{ + "type": "object", + "required": [ + "username", + "token" + ], + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "minLength": 1 + }, + "token": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSNamecom() Provider { + return Provider{ + AcmeshName: "dns_namecom", + Schema: nameComSchema, + Fields: []providerField{ + { + Name: "Username", + Type: "text", + MetaKey: "username", + EnvKey: "Namecom_Username", + IsRequired: true, + }, + { + Name: "Token", + Type: "text", + MetaKey: "token", + EnvKey: "Namecom_Token", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_nsone.go b/backend/internal/dnsproviders/dns_nsone.go new file mode 100644 index 00000000..b02a1788 --- /dev/null +++ b/backend/internal/dnsproviders/dns_nsone.go @@ -0,0 +1,18 @@ +package dnsproviders + +func getDNSOne() Provider { + return Provider{ + AcmeshName: "dns_nsone", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "NS1_Key", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_pdns.go b/backend/internal/dnsproviders/dns_pdns.go new file mode 100644 index 00000000..35db50ee --- /dev/null +++ b/backend/internal/dnsproviders/dns_pdns.go @@ -0,0 +1,69 @@ +package dnsproviders + +const powerDNSSchema = ` +{ + "type": "object", + "required": [ + "url", + "server_id", + "token", + "ttl" + ], + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "minLength": 1 + }, + "server_id": { + "type": "string", + "minLength": 1 + }, + "token": { + "type": "string", + "minLength": 1 + }, + "ttl": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSPDNS() Provider { + return Provider{ + AcmeshName: "dns_pdns", + Schema: powerDNSSchema, + Fields: []providerField{ + { + Name: "URL", + Type: "text", + MetaKey: "url", + EnvKey: "PDNS_Url", + IsRequired: true, + }, + { + Name: "Server ID", + Type: "text", + MetaKey: "server_id", + EnvKey: "PDNS_ServerId", + IsRequired: true, + }, + { + Name: "Token", + Type: "text", + MetaKey: "token", + EnvKey: "PDNS_Token", + IsRequired: true, + }, + { + Name: "TTL", + Type: "number", + MetaKey: "ttl", + EnvKey: "PDNS_Ttl", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_unoeuro.go b/backend/internal/dnsproviders/dns_unoeuro.go new file mode 100644 index 00000000..8524a89b --- /dev/null +++ b/backend/internal/dnsproviders/dns_unoeuro.go @@ -0,0 +1,47 @@ +package dnsproviders + +const unoEuroSchema = ` +{ + "type": "object", + "required": [ + "api_key", + "user" + ], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + }, + "user": { + "type": "string", + "minLength": 1 + } + } +} +` + +func getDNSUnoeuro() Provider { + return Provider{ + AcmeshName: "dns_unoeuro", + Schema: unoEuroSchema, + Fields: []providerField{ + { + Name: "Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "UNO_Key", + IsRequired: true, + IsSecret: true, + }, + { + Name: "User", + Type: "text", + MetaKey: "user", + EnvKey: "UNO_User", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_vscale.go b/backend/internal/dnsproviders/dns_vscale.go new file mode 100644 index 00000000..59463cf8 --- /dev/null +++ b/backend/internal/dnsproviders/dns_vscale.go @@ -0,0 +1,17 @@ +package dnsproviders + +func getDNSVscale() Provider { + return Provider{ + AcmeshName: "dns_vscale", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "API Key", + Type: "password", + MetaKey: "api_key", + EnvKey: "VSCALE_API_KEY", + IsRequired: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_yandex.go b/backend/internal/dnsproviders/dns_yandex.go new file mode 100644 index 00000000..7812069e --- /dev/null +++ b/backend/internal/dnsproviders/dns_yandex.go @@ -0,0 +1,18 @@ +package dnsproviders + +func getDNSYandex() Provider { + return Provider{ + AcmeshName: "dns_yandex", + Schema: commonKeySchema, + Fields: []providerField{ + { + Name: "Token", + Type: "password", + MetaKey: "api_key", + EnvKey: "PDD_Token", + IsRequired: true, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/entity/auth/methods.go b/backend/internal/entity/auth/methods.go new file mode 100644 index 00000000..9cfb7faa --- /dev/null +++ b/backend/internal/entity/auth/methods.go @@ -0,0 +1,82 @@ +package auth + +import ( + goerrors "errors" + "fmt" + + "npm/internal/database" +) + +// GetByID finds a auth by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByUserIDType finds a user by email +func GetByUserIDType(userID int, authType string) (Model, error) { + var m Model + err := m.LoadByUserIDType(userID, authType) + return m, err +} + +// Create will create a Auth from this model +func Create(auth *Model) (int, error) { + if auth.ID != 0 { + return 0, goerrors.New("Cannot create auth when model already has an ID") + } + + auth.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + secret, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :secret, + :is_deleted + )`, auth) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Auth from this model +func Update(auth *Model) error { + if auth.ID == 0 { + return goerrors.New("Cannot update auth when model doesn't have an ID") + } + + auth.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + type = :type, + secret = :secret, + is_deleted = :is_deleted + WHERE id = :id`, auth) + + return err +} diff --git a/backend/internal/entity/auth/model.go b/backend/internal/entity/auth/model.go new file mode 100644 index 00000000..b5640bbf --- /dev/null +++ b/backend/internal/entity/auth/model.go @@ -0,0 +1,98 @@ +package auth + +import ( + goerrors "errors" + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" + + "golang.org/x/crypto/bcrypt" +) + +const ( + tableName = "auth" + + // TypePassword is the Password Type + TypePassword = "password" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id"` + UserID int `json:"user_id" db:"user_id"` + Type string `json:"type" db:"type"` + Secret string `json:"secret,omitempty" db:"secret"` + CreatedOn types.DBDate `json:"created_on" db:"created_on"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", tableName) + params := []interface{}{id} + return m.getByQuery(query, params) +} + +// LoadByUserIDType will load from an ID +func (m *Model) LoadByUserIDType(userID int, authType string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE user_id = ? AND type = ? LIMIT 1", tableName) + params := []interface{}{userID, authType} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// SetPassword will generate a hashed password based on given string +func (m *Model) SetPassword(password string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost+2) + if err != nil { + return err + } + + m.Type = TypePassword + m.Secret = string(hash) + + return nil +} + +// ValidateSecret will check if a given secret matches the encrypted secret +func (m *Model) ValidateSecret(secret string) error { + if m.Type != TypePassword { + return goerrors.New("Could not validate Secret, auth type is not a Password") + } + + err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret)) + if err != nil { + return goerrors.New("Invalid Password") + } + + return nil +} diff --git a/backend/internal/entity/certificate/filters.go b/backend/internal/entity/certificate/filters.go new file mode 100644 index 00000000..1c034c15 --- /dev/null +++ b/backend/internal/entity/certificate/filters.go @@ -0,0 +1,25 @@ +package certificate + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/certificate/methods.go b/backend/internal/entity/certificate/methods.go new file mode 100644 index 00000000..e24181be --- /dev/null +++ b/backend/internal/entity/certificate/methods.go @@ -0,0 +1,174 @@ +package certificate + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(certificate *Model) (int, error) { + if certificate.ID != 0 { + return 0, goerrors.New("Cannot create certificate when model already has an ID") + } + + certificate.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + certificate_authority_id, + dns_provider_id, + name, + domain_names, + expires_on, + status, + meta, + is_ecc, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :certificate_authority_id, + :dns_provider_id, + :name, + :domain_names, + :expires_on, + :status, + :meta, + :is_ecc, + :is_deleted + )`, certificate) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Auth from this model +func Update(certificate *Model) error { + if certificate.ID == 0 { + return goerrors.New("Cannot update certificate when model doesn't have an ID") + } + + certificate.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + type = :type, + user_id = :user_id, + certificate_authority_id = :certificate_authority_id, + dns_provider_id = :dns_provider_id, + name = :name, + domain_names = :domain_names, + expires_on = :expires_on, + status = :status, + meta = :meta, + is_ecc = :is_ecc, + is_deleted = :is_deleted + WHERE id = :id`, certificate) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} + +// GetByStatus will select rows that are ready for requesting +func GetByStatus(status string) ([]Model, error) { + models := make([]Model, 0) + db := database.GetInstance() + + query := fmt.Sprintf(` + SELECT + t.* + FROM "%s" t + INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id" + WHERE + t."type" IN ("http", "dns") AND + t."status" = ? AND + t."certificate_authority_id" > 0 AND + t."is_deleted" = 0 + `, tableName) + + params := []interface{}{StatusReady} + err := db.Select(&models, query, params...) + if err != nil && err != sql.ErrNoRows { + logger.Error("GetByStatusError", err) + logger.Debug("Query: %s -- %+v", query, params) + } + + return models, err +} diff --git a/backend/internal/entity/certificate/model.go b/backend/internal/entity/certificate/model.go new file mode 100644 index 00000000..556e1699 --- /dev/null +++ b/backend/internal/entity/certificate/model.go @@ -0,0 +1,266 @@ +package certificate + +import ( + "errors" + "fmt" + "os" + "regexp" + "strings" + "time" + + "npm/internal/acme" + "npm/internal/config" + "npm/internal/database" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/logger" + "npm/internal/types" +) + +const ( + tableName = "certificate" + + // TypeCustom custom cert type + TypeCustom = "custom" + // TypeHTTP http cert type + TypeHTTP = "http" + // TypeDNS dns cert type + TypeDNS = "dns" + // TypeMkcert mkcert cert type + TypeMkcert = "mkcert" + + // StatusReady is ready for certificate to be requested + StatusReady = "ready" + // StatusRequesting is process of being requested + StatusRequesting = "requesting" + // StatusFailed is a certicifate that failed to request + StatusFailed = "failed" + // StatusProvided is a certificate provided and ready for actual use + StatusProvided = "provided" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + ExpiresOn types.NullableDBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"` + Type string `json:"type" db:"type" filter:"type,string"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + CertificateAuthorityID int `json:"certificate_authority_id" db:"certificate_authority_id" filter:"certificate_authority_id,integer"` + DNSProviderID int `json:"dns_provider_id" db:"dns_provider_id" filter:"dns_provider_id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + Status string `json:"status" db:"status" filter:"status,string"` + ErrorMessage string `json:"error_message" db:"error_message" filter:"error_message,string"` + Meta types.JSONB `json:"-" db:"meta"` + IsECC int `json:"is_ecc" db:"is_ecc" filter:"is_ecc,integer"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions: + CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty"` + DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if !m.Validate() { + return fmt.Errorf("Certificate data is incorrect or incomplete for this type") + } + + if !m.ValidateWildcardSupport() { + return fmt.Errorf("Cannot use Wildcard domains with this CA") + } + + m.setDefaultStatus() + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// Validate will make sure the data given is expected. This object is a bit complicated, +// as there could be multiple combinations of values. +func (m *Model) Validate() bool { + switch m.Type { + case TypeCustom: + // TODO: make sure meta contains required fields + return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0 + + case TypeHTTP: + return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0 + + case TypeDNS: + return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0 + + case TypeMkcert: + return true + + default: + return false + } +} + +// ValidateWildcardSupport will ensure that the CA given supports wildcards, +// only if the domains on this object have at least 1 wildcard +func (m *Model) ValidateWildcardSupport() bool { + domains, err := m.DomainNames.AsStringArray() + if err != nil { + logger.Error("ValidateWildcardSupportError", err) + return false + } + + hasWildcard := false + for _, domain := range domains { + if strings.Contains(domain, "*") { + hasWildcard = true + } + } + + if hasWildcard { + m.Expand() + if !m.CertificateAuthority.IsWildcardSupported { + return false + } + } + + return true +} + +func (m *Model) setDefaultStatus() { + if m.ID == 0 { + // It's a new certificate + if m.Type == TypeCustom { + m.Status = StatusProvided + } else { + m.Status = StatusReady + } + } +} + +// Expand will populate attached objects for the model +func (m *Model) Expand() { + if m.CertificateAuthorityID > 0 { + certificateAuthority, _ := certificateauthority.GetByID(m.CertificateAuthorityID) + m.CertificateAuthority = &certificateAuthority + } + if m.DNSProviderID > 0 { + dnsProvider, _ := dnsprovider.GetByID(m.DNSProviderID) + m.DNSProvider = &dnsProvider + } +} + +// GetCertificateLocations will return the paths on disk where the SSL +// certs should or would be. +// Returns: (key, fullchain, certFolder) +func (m *Model) GetCertificateLocations() (string, string, string) { + if m.ID == 0 { + logger.Error("GetCertificateLocationsError", errors.New("GetCertificateLocations called before certificate was saved")) + return "", "", "" + } + + certFolder := fmt.Sprintf("%s/certificates", config.Configuration.DataFolder) + + // Generate a unique folder name for this cert + m1 := regexp.MustCompile(`[^A-Za-z0-9\.]`) + + niceName := m1.ReplaceAllString(m.Name, "_") + if len(niceName) > 20 { + niceName = niceName[:20] + } + folderName := fmt.Sprintf("%d-%s", m.ID, niceName) + + return fmt.Sprintf("%s/%s/key.pem", certFolder, folderName), + fmt.Sprintf("%s/%s/fullchain.pem", certFolder, folderName), + fmt.Sprintf("%s/%s", certFolder, folderName) +} + +// Request makes a certificate request +func (m *Model) Request() error { + logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name) + + m.Expand() + m.Status = StatusRequesting + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + + // do request + domains, err := m.DomainNames.AsStringArray() + if err != nil { + logger.Error("CertificateRequestError", err) + return err + } + + certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations() + + // ensure certFolder is created + if err := os.MkdirAll(certFolder, os.ModePerm); err != nil { + logger.Error("CreateFolderError", err) + return err + } + + errMsg, err := acme.RequestCert(domains, m.Type, certFullchainFile, certKeyFile, m.DNSProvider, m.CertificateAuthority, true) + if err != nil { + m.Status = StatusFailed + m.ErrorMessage = errMsg + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + return nil + } + + // If done + m.Status = StatusProvided + t := time.Now() + m.ExpiresOn.Time = &t // todo + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + + logger.Info("Request for certificate for: #%d %v was completed", m.ID, m.Name) + return nil +} diff --git a/backend/internal/entity/certificate/structs.go b/backend/internal/entity/certificate/structs.go new file mode 100644 index 00000000..a9b99369 --- /dev/null +++ b/backend/internal/entity/certificate/structs.go @@ -0,0 +1,15 @@ +package certificate + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/certificateauthority/filters.go b/backend/internal/entity/certificateauthority/filters.go new file mode 100644 index 00000000..a16ba01e --- /dev/null +++ b/backend/internal/entity/certificateauthority/filters.go @@ -0,0 +1,25 @@ +package certificateauthority + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/certificateauthority/methods.go b/backend/internal/entity/certificateauthority/methods.go new file mode 100644 index 00000000..28941e21 --- /dev/null +++ b/backend/internal/entity/certificateauthority/methods.go @@ -0,0 +1,134 @@ +package certificateauthority + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(ca *Model) (int, error) { + if ca.ID != 0 { + return 0, goerrors.New("Cannot create certificate authority when model already has an ID") + } + + ca.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + acmesh_server, + ca_bundle, + max_domains, + is_wildcard_supported, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :name, + :acmesh_server, + :ca_bundle, + :max_domains, + :is_wildcard_supported, + :is_deleted + )`, ca) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a row from this model +func Update(ca *Model) error { + if ca.ID == 0 { + return goerrors.New("Cannot update certificate authority when model doesn't have an ID") + } + + ca.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + acmesh_server = :acmesh_server, + ca_bundle = :ca_bundle, + max_domains = :max_domains, + is_wildcard_supported = :is_wildcard_supported, + is_deleted = :is_deleted + WHERE id = :id`, ca) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("ListCertificateAuthoritiesError", queryErr) + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Error("ListCertificateAuthoritiesError", err) + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/certificateauthority/model.go b/backend/internal/entity/certificateauthority/model.go new file mode 100644 index 00000000..e5817d0a --- /dev/null +++ b/backend/internal/entity/certificateauthority/model.go @@ -0,0 +1,88 @@ +package certificateauthority + +import ( + goerrors "errors" + "fmt" + "os" + "path/filepath" + "time" + + "npm/internal/database" + "npm/internal/errors" + "npm/internal/types" +) + +const ( + tableName = "certificate_authority" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + AcmeshServer string `json:"acmesh_server" db:"acmesh_server" filter:"acmesh_server,string"` + CABundle string `json:"ca_bundle" db:"ca_bundle" filter:"ca_bundle,string"` + MaxDomains int `json:"max_domains" db:"max_domains" filter:"max_domains,integer"` + IsWildcardSupported bool `json:"is_wildcard_supported" db:"is_wildcard_supported" filter:"is_wildcard_supported,boolean"` + IsReadonly bool `json:"is_readonly" db:"is_readonly" filter:"is_readonly,boolean"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// Check will ensure the ca bundle path exists if it's set +func (m *Model) Check() error { + var err error + + if m.CABundle != "" { + if _, fileerr := os.Stat(filepath.Clean(m.CABundle)); goerrors.Is(fileerr, os.ErrNotExist) { + err = errors.ErrCABundleDoesNotExist + } + } + + return err +} diff --git a/backend/internal/entity/certificateauthority/structs.go b/backend/internal/entity/certificateauthority/structs.go new file mode 100644 index 00000000..85e3521a --- /dev/null +++ b/backend/internal/entity/certificateauthority/structs.go @@ -0,0 +1,15 @@ +package certificateauthority + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/dnsprovider/filters.go b/backend/internal/entity/dnsprovider/filters.go new file mode 100644 index 00000000..30ab6c52 --- /dev/null +++ b/backend/internal/entity/dnsprovider/filters.go @@ -0,0 +1,25 @@ +package dnsprovider + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/dnsprovider/methods.go b/backend/internal/entity/dnsprovider/methods.go new file mode 100644 index 00000000..c1a1e0e1 --- /dev/null +++ b/backend/internal/entity/dnsprovider/methods.go @@ -0,0 +1,134 @@ +package dnsprovider + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(provider *Model) (int, error) { + if provider.ID != 0 { + return 0, goerrors.New("Cannot create dns provider when model already has an ID") + } + + provider.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + name, + acmesh_name, + dns_sleep, + meta, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :name, + :acmesh_name, + :dns_sleep, + :meta, + :is_deleted + )`, provider) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a row from this model +func Update(provider *Model) error { + if provider.ID == 0 { + return goerrors.New("Cannot update dns provider when model doesn't have an ID") + } + + provider.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + name = :name, + acmesh_name = :acmesh_name, + dns_sleep = :dns_sleep, + meta = :meta, + is_deleted = :is_deleted + WHERE id = :id`, provider) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("ListDnsProvidersError", queryErr) + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Error("ListDnsProvidersError", err) + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/dnsprovider/model.go b/backend/internal/entity/dnsprovider/model.go new file mode 100644 index 00000000..34253bea --- /dev/null +++ b/backend/internal/entity/dnsprovider/model.go @@ -0,0 +1,101 @@ +package dnsprovider + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/dnsproviders" + "npm/internal/logger" + "npm/internal/types" +) + +const ( + tableName = "dns_provider" +) + +// Model is the user model +// Also see: https://github.com/acmesh-official/acme.sh/wiki/dnscheck +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + AcmeshName string `json:"acmesh_name" db:"acmesh_name" filter:"acmesh_name,string"` + DNSSleep int `json:"dns_sleep" db:"dns_sleep" filter:"dns_sleep,integer"` + Meta types.JSONB `json:"meta" db:"meta"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// GetAcmeShEnvVars returns the env vars required for acme.sh dns cert requests +func (m *Model) GetAcmeShEnvVars() ([]string, error) { + logger.Debug("GetAcmeShEnvVars for: %s", m.AcmeshName) + // First, fetch the provider obj with this AcmeShName + acmeDNSProvider, err := dnsproviders.Get(m.AcmeshName) + logger.Debug("acmeDNSProvider: %+v", acmeDNSProvider) + if err != nil { + logger.Error("GetAcmeShEnvVarsError", err) + return nil, err + } + + envs := make([]string, 0) + + // Then, using the meta, convert to env vars + envPairs := acmeDNSProvider.GetAcmeEnvVars(m.Meta.Decoded) + logger.Debug("meta: %+v", m.Meta) + logger.Debug("envPairs: %+v", envPairs) + for envName, envValue := range envPairs { + envs = append(envs, fmt.Sprintf(`%s=%v`, envName, envValue)) + } + + return envs, nil +} diff --git a/backend/internal/entity/dnsprovider/model_test.go b/backend/internal/entity/dnsprovider/model_test.go new file mode 100644 index 00000000..5c4fef28 --- /dev/null +++ b/backend/internal/entity/dnsprovider/model_test.go @@ -0,0 +1,82 @@ +package dnsprovider + +import ( + "encoding/json" + "npm/internal/types" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestModelGetAcmeShEnvVars(t *testing.T) { + type want struct { + envs []string + err error + } + + tests := []struct { + name string + dnsProvider Model + metaJSON string + want want + }{ + { + name: "dns_aws", + dnsProvider: Model{ + AcmeshName: "dns_aws", + }, + metaJSON: `{"access_key_id":"sdfsdfsdfljlbjkljlkjsdfoiwje","access_key":"xxxxxxx"}`, + want: want{ + envs: []string{ + `AWS_ACCESS_KEY_ID=sdfsdfsdfljlbjkljlkjsdfoiwje`, + `AWS_SECRET_ACCESS_KEY=xxxxxxx`, + }, + err: nil, + }, + }, + { + name: "dns_cf", + dnsProvider: Model{ + AcmeshName: "dns_cf", + }, + metaJSON: `{"api_key":"sdfsdfsdfljlbjkljlkjsdfoiwje","email":"me@example.com","token":"dkfjghdk","account_id":"hgbdjfg","zone_id":"ASDASD"}`, + want: want{ + envs: []string{ + `CF_Token=dkfjghdk`, + `CF_Account_ID=hgbdjfg`, + `CF_Zone_ID=ASDASD`, + `CF_Key=sdfsdfsdfljlbjkljlkjsdfoiwje`, + `CF_Email=me@example.com`, + }, + err: nil, + }, + }, + { + name: "dns_duckdns", + dnsProvider: Model{ + AcmeshName: "dns_duckdns", + }, + metaJSON: `{"api_key":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}`, + want: want{ + envs: []string{ + `DuckDNS_Token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`, + }, + err: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var meta types.JSONB + err := json.Unmarshal([]byte(tt.metaJSON), &meta.Decoded) + assert.Equal(t, nil, err) + tt.dnsProvider.Meta = meta + envs, err := tt.dnsProvider.GetAcmeShEnvVars() + assert.Equal(t, tt.want.err, err) + for _, i := range tt.want.envs { + assert.Contains(t, envs, i) + } + }) + } +} diff --git a/backend/internal/entity/dnsprovider/structs.go b/backend/internal/entity/dnsprovider/structs.go new file mode 100644 index 00000000..835c947b --- /dev/null +++ b/backend/internal/entity/dnsprovider/structs.go @@ -0,0 +1,15 @@ +package dnsprovider + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for the list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/filters.go b/backend/internal/entity/filters.go new file mode 100644 index 00000000..9709fa16 --- /dev/null +++ b/backend/internal/entity/filters.go @@ -0,0 +1,158 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" + + "npm/internal/model" +) + +// FilterMapFunction is a filter map function +type FilterMapFunction func(value []string) []string + +// FilterTagName tag name user for filter pickups +const FilterTagName = "filter" + +// DBTagName tag name user for field name pickups +const DBTagName = "db" + +// GenerateSQLFromFilters will return a Query and params for use as WHERE clause in SQL queries +// This will use a AND where clause approach. +func GenerateSQLFromFilters(filters []model.Filter, fieldMap map[string]string, fieldMapFunctions map[string]FilterMapFunction) (string, []interface{}) { + clauses := make([]string, 0) + params := make([]interface{}, 0) + + for _, filter := range filters { + // Lookup this filter field from the functions map + if _, ok := fieldMapFunctions[filter.Field]; ok { + filter.Value = fieldMapFunctions[filter.Field](filter.Value) + } + + // Lookup this filter field from the name map + if _, ok := fieldMap[filter.Field]; ok { + filter.Field = fieldMap[filter.Field] + } + + // Special case for LIKE queries, the column needs to be uppercase for comparison + fieldName := fmt.Sprintf("`%s`", filter.Field) + if strings.ToLower(filter.Modifier) == "contains" || strings.ToLower(filter.Modifier) == "starts" || strings.ToLower(filter.Modifier) == "ends" { + fieldName = fmt.Sprintf("UPPER(`%s`)", filter.Field) + } + + clauses = append(clauses, fmt.Sprintf("%s %s", fieldName, getSQLAssignmentFromModifier(filter, ¶ms))) + } + + return strings.Join(clauses, " AND "), params +} + +func getSQLAssignmentFromModifier(filter model.Filter, params *[]interface{}) string { + var clause string + + // Quick hacks + if filter.Modifier == "in" && len(filter.Value) == 1 { + filter.Modifier = "equals" + } else if filter.Modifier == "notin" && len(filter.Value) == 1 { + filter.Modifier = "not" + } + + switch strings.ToLower(filter.Modifier) { + default: + clause = "= ?" + case "not": + clause = "!= ?" + case "min": + clause = ">= ?" + case "max": + clause = "<= ?" + case "greater": + clause = "> ?" + case "lesser": + clause = "< ?" + + // LIKE modifiers: + case "contains": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE '%' || ? || '%'" + case "starts": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE ? || '%'" + case "ends": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE '%' || ?" + + // Array parameter modifiers: + case "in": + s, p := buildInArray(filter.Value) + *params = append(*params, p...) + return fmt.Sprintf("IN (%s)", s) + case "notin": + s, p := buildInArray(filter.Value) + *params = append(*params, p...) + return fmt.Sprintf("NOT IN (%s)", s) + } + + *params = append(*params, filter.Value[0]) + return clause +} + +// GetFilterMap returns the filter map +func GetFilterMap(m interface{}) map[string]string { + var filterMap = make(map[string]string) + + // TypeOf returns the reflection Type that represents the dynamic type of variable. + // If variable is a nil interface value, TypeOf returns nil. + t := reflect.TypeOf(m) + + // Iterate over all available fields and read the tag value + for i := 0; i < t.NumField(); i++ { + // Get the field, returns https://golang.org/pkg/reflect/#StructField + field := t.Field(i) + + // Get the field tag value + filterTag := field.Tag.Get(FilterTagName) + dbTag := field.Tag.Get(DBTagName) + if filterTag != "" && dbTag != "" && dbTag != "-" && filterTag != "-" { + // Filter tag can be a 2 part thing: name,type + // ie: account_id,integer + // So we need to split and use the first part + parts := strings.Split(filterTag, ",") + filterMap[parts[0]] = dbTag + filterMap[filterTag] = dbTag + } + } + + return filterMap +} + +// GetDBColumns returns the db columns +func GetDBColumns(m interface{}) []string { + var columns []string + t := reflect.TypeOf(m) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + dbTag := field.Tag.Get(DBTagName) + if dbTag != "" && dbTag != "-" { + columns = append(columns, dbTag) + } + } + + return columns +} + +func buildInArray(items []string) (string, []interface{}) { + // Query string placeholder + strs := make([]string, len(items)) + for i := 0; i < len(items); i++ { + strs[i] = "?" + } + + // Params as interface + params := make([]interface{}, len(items)) + for i, v := range items { + params[i] = v + } + + return strings.Join(strs, ", "), params +} diff --git a/backend/internal/entity/filters_schema.go b/backend/internal/entity/filters_schema.go new file mode 100644 index 00000000..9686293b --- /dev/null +++ b/backend/internal/entity/filters_schema.go @@ -0,0 +1,223 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" +) + +// GetFilterSchema creates a jsonschema for validating filters, based on the model +// object given and by reading the struct "filter" tags. +func GetFilterSchema(m interface{}) string { + var schemas []string + t := reflect.TypeOf(m) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + filterTag := field.Tag.Get(FilterTagName) + + if filterTag != "" && filterTag != "-" { + // split out tag value "field,filtreType" + // with a default filter type of string + items := strings.Split(filterTag, ",") + if len(items) == 1 { + items = append(items, "string") + } + + switch items[1] { + case "int": + fallthrough + case "integer": + schemas = append(schemas, intFieldSchema(items[0])) + case "bool": + fallthrough + case "boolean": + schemas = append(schemas, boolFieldSchema(items[0])) + case "date": + schemas = append(schemas, dateFieldSchema(items[0])) + case "regex": + if len(items) < 3 { + items = append(items, ".*") + } + schemas = append(schemas, regexFieldSchema(items[0], items[2])) + + default: + schemas = append(schemas, stringFieldSchema(items[0])) + } + } + } + + return newFilterSchema(schemas) +} + +// newFilterSchema is the main method to specify a new Filter Schema for use in Middleware +func newFilterSchema(fieldSchemas []string) string { + return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", ")) +} + +// boolFieldSchema returns the Field Schema for a Boolean accepted value field +func boolFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, boolModifiers, filterBool, filterBool) +} + +// intFieldSchema returns the Field Schema for a Integer accepted value field +func intFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// stringFieldSchema returns the Field Schema for a String accepted value field +func stringFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, stringModifiers, filterString, filterString) +} + +// regexFieldSchema returns the Field Schema for a String accepted value field matching a Regex +func regexFieldSchema(fieldName string, regex string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "%s" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "%s" + } + } + ] + } + } + }`, fieldName, stringModifiers, regex, regex) +} + +// dateFieldSchema returns the Field Schema for a String accepted value field matching a Date format +func dateFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +const allModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$" +}` + +const boolModifiers = `{ + "type": "string", + "pattern": "^(equals|not)$" +}` + +const stringModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin)$" +}` + +const filterBool = `{ + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" +}` + +const filterString = `{ + "type": "string", + "minLength": 1 +}` + +const baseFilterSchema = `{ + "type": "array", + "items": { + "oneOf": [ + %s + ] + } +}` diff --git a/backend/internal/entity/host/filters.go b/backend/internal/entity/host/filters.go new file mode 100644 index 00000000..7b023917 --- /dev/null +++ b/backend/internal/entity/host/filters.go @@ -0,0 +1,25 @@ +package host + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/host/methods.go b/backend/internal/entity/host/methods.go new file mode 100644 index 00000000..ddf64dc1 --- /dev/null +++ b/backend/internal/entity/host/methods.go @@ -0,0 +1,183 @@ +package host + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Host by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// create will create a Host from this model +func create(host *Model) (int, error) { + if host.ID != 0 { + return 0, goerrors.New("Cannot create host when model already has an ID") + } + + host.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + host_template_id, + listen_interface, + domain_names, + upstream_id, + certificate_id, + access_list_id, + ssl_forced, + caching_enabled, + block_exploits, + allow_websocket_upgrade, + http2_support, + hsts_enabled, + hsts_subdomains, + paths, + upstream_options, + advanced_config, + is_disabled, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :host_template_id, + :listen_interface, + :domain_names, + :upstream_id, + :certificate_id, + :access_list_id, + :ssl_forced, + :caching_enabled, + :block_exploits, + :allow_websocket_upgrade, + :http2_support, + :hsts_enabled, + :hsts_subdomains, + :paths, + :upstream_options, + :advanced_config, + :is_disabled, + :is_deleted + )`, host) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// update will Update a Host from this model +func update(host *Model) error { + if host.ID == 0 { + return goerrors.New("Cannot update host when model doesn't have an ID") + } + + host.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + type = :type, + host_template_id = :host_template_id, + listen_interface = :listen_interface, + domain_names = :domain_names, + upstream_id = :upstream_id, + certificate_id = :certificate_id, + access_list_id = :access_list_id, + ssl_forced = :ssl_forced, + caching_enabled = :caching_enabled, + block_exploits = :block_exploits, + allow_websocket_upgrade = :allow_websocket_upgrade, + http2_support = :http2_support, + hsts_enabled = :hsts_enabled, + hsts_subdomains = :hsts_subdomains, + paths = :paths, + upstream_options = :upstream_options, + advanced_config = :advanced_config, + is_disabled = :is_disabled, + is_deleted = :is_deleted + WHERE id = :id`, host) + + return err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "domain_names", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + if expand != nil { + for idx := range items { + expandErr := items[idx].Expand(expand) + if expandErr != nil { + logger.Error("HostsExpansionError", expandErr) + } + } + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/host/model.go b/backend/internal/entity/host/model.go new file mode 100644 index 00000000..e9eb88fe --- /dev/null +++ b/backend/internal/entity/host/model.go @@ -0,0 +1,120 @@ +package host + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/entity/certificate" + "npm/internal/entity/user" + "npm/internal/types" + "npm/internal/util" +) + +const ( + tableName = "host" + + // ProxyHostType is self explanatory + ProxyHostType = "proxy" + // RedirectionHostType is self explanatory + RedirectionHostType = "redirection" + // DeadHostType is self explanatory + DeadHostType = "dead" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Type string `json:"type" db:"type" filter:"type,string"` + HostTemplateID int `json:"host_template_id" db:"host_template_id" filter:"host_template_id,integer"` + ListenInterface string `json:"listen_interface" db:"listen_interface" filter:"listen_interface,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"` + CertificateID int `json:"certificate_id" db:"certificate_id" filter:"certificate_id,integer"` + AccessListID int `json:"access_list_id" db:"access_list_id" filter:"access_list_id,integer"` + SSLForced bool `json:"ssl_forced" db:"ssl_forced" filter:"ssl_forced,boolean"` + CachingEnabled bool `json:"caching_enabled" db:"caching_enabled" filter:"caching_enabled,boolean"` + BlockExploits bool `json:"block_exploits" db:"block_exploits" filter:"block_exploits,boolean"` + AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" db:"allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"` + HTTP2Support bool `json:"http2_support" db:"http2_support" filter:"http2_support,boolean"` + HSTSEnabled bool `json:"hsts_enabled" db:"hsts_enabled" filter:"hsts_enabled,boolean"` + HSTSSubdomains bool `json:"hsts_subdomains" db:"hsts_subdomains" filter:"hsts_subdomains,boolean"` + Paths string `json:"paths" db:"paths" filter:"paths,string"` + UpstreamOptions string `json:"upstream_options" db:"upstream_options" filter:"upstream_options,string"` + AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"` + IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions + Certificate *certificate.Model `json:"certificate,omitempty"` + User *user.Model `json:"user,omitempty"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = create(m) + } else { + err = update(m) + } + + return err +} + +// Delete will mark a host as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + if util.SliceContainsItem(items, "user") && m.ID > 0 { + var usr user.Model + usr, err = user.GetByID(m.UserID) + m.User = &usr + } + + if util.SliceContainsItem(items, "certificate") && m.CertificateID > 0 { + var cert certificate.Model + cert, err = certificate.GetByID(m.CertificateID) + m.Certificate = &cert + } + + return err +} diff --git a/backend/internal/entity/host/structs.go b/backend/internal/entity/host/structs.go new file mode 100644 index 00000000..dda3d6fe --- /dev/null +++ b/backend/internal/entity/host/structs.go @@ -0,0 +1,15 @@ +package host + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/hosttemplate/filters.go b/backend/internal/entity/hosttemplate/filters.go new file mode 100644 index 00000000..3b4d512e --- /dev/null +++ b/backend/internal/entity/hosttemplate/filters.go @@ -0,0 +1,25 @@ +package hosttemplate + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/hosttemplate/methods.go b/backend/internal/entity/hosttemplate/methods.go new file mode 100644 index 00000000..e75fcfd3 --- /dev/null +++ b/backend/internal/entity/hosttemplate/methods.go @@ -0,0 +1,129 @@ +package hosttemplate + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Host by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a Host from this model +func Create(host *Model) (int, error) { + if host.ID != 0 { + return 0, goerrors.New("Cannot create host template when model already has an ID") + } + + host.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + name, + host_type, + template, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :name, + :host_type, + :template, + :is_deleted + )`, host) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Host from this model +func Update(host *Model) error { + if host.ID == 0 { + return goerrors.New("Cannot update host template when model doesn't have an ID") + } + + host.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + name = :name, + host_type = :host_type, + template = :template, + is_deleted = :is_deleted + WHERE id = :id`, host) + + return err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "created_on", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/hosttemplate/model.go b/backend/internal/entity/hosttemplate/model.go new file mode 100644 index 00000000..c791537a --- /dev/null +++ b/backend/internal/entity/hosttemplate/model.go @@ -0,0 +1,73 @@ +package hosttemplate + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "host_template" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Type string `json:"host_type" db:"host_type" filter:"host_type,string"` + Template string `json:"template" db:"template" filter:"template,string"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a host as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/hosttemplate/structs.go b/backend/internal/entity/hosttemplate/structs.go new file mode 100644 index 00000000..4cc54b91 --- /dev/null +++ b/backend/internal/entity/hosttemplate/structs.go @@ -0,0 +1,15 @@ +package hosttemplate + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/lists_query.go b/backend/internal/entity/lists_query.go new file mode 100644 index 00000000..345f8584 --- /dev/null +++ b/backend/internal/entity/lists_query.go @@ -0,0 +1,80 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" + + "npm/internal/database" + "npm/internal/model" +) + +// ListQueryBuilder should be able to return the query and params to get items agnostically based +// on given params. +func ListQueryBuilder(modelExample interface{}, tableName string, pageInfo *model.PageInfo, defaultSort model.Sort, filters []model.Filter, filterMapFunctions map[string]FilterMapFunction, returnCount bool) (string, []interface{}) { + var queryStrings []string + var whereStrings []string + var params []interface{} + + if returnCount { + queryStrings = append(queryStrings, "SELECT COUNT(*)") + } else { + queryStrings = append(queryStrings, "SELECT *") + } + + // nolint: gosec + queryStrings = append(queryStrings, fmt.Sprintf("FROM `%s`", tableName)) + + // Append filters to where clause: + if filters != nil { + filterMap := GetFilterMap(modelExample) + filterQuery, filterParams := GenerateSQLFromFilters(filters, filterMap, filterMapFunctions) + whereStrings = []string{filterQuery} + params = append(params, filterParams...) + } + + // Add is deletee check if model has the field + if hasDeletedField(modelExample) { + params = append(params, 0) + whereStrings = append(whereStrings, "`is_deleted` = ?") + } + + // Append where clauses to query + if len(whereStrings) > 0 { + // nolint: gosec + queryStrings = append(queryStrings, fmt.Sprintf("WHERE %s", strings.Join(whereStrings, " AND "))) + } + + if !returnCount { + var orderBy string + columns := GetDBColumns(modelExample) + orderBy, pageInfo.Sort = database.BuildOrderBySQL(columns, &pageInfo.Sort) + + if orderBy != "" { + queryStrings = append(queryStrings, orderBy) + } else { + pageInfo.Sort = append(pageInfo.Sort, defaultSort) + queryStrings = append(queryStrings, fmt.Sprintf("ORDER BY `%v` COLLATE NOCASE %v", defaultSort.Field, defaultSort.Direction)) + } + + params = append(params, pageInfo.Offset) + params = append(params, pageInfo.Limit) + queryStrings = append(queryStrings, "LIMIT ?, ?") + } + + return strings.Join(queryStrings, " "), params +} + +func hasDeletedField(modelExample interface{}) bool { + t := reflect.TypeOf(modelExample) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + dbTag := field.Tag.Get(DBTagName) + if dbTag == "is_deleted" { + return true + } + } + + return false +} diff --git a/backend/internal/entity/setting/apply.go b/backend/internal/entity/setting/apply.go new file mode 100644 index 00000000..29428975 --- /dev/null +++ b/backend/internal/entity/setting/apply.go @@ -0,0 +1,19 @@ +package setting + +import ( + "npm/internal/config" + "npm/internal/logger" +) + +// ApplySettings will load settings from the DB and apply them where required +func ApplySettings() { + logger.Debug("Applying Settings") + + // Error-reporting + m, err := GetByName("error-reporting") + if err != nil { + logger.Error("ApplySettingsError", err) + } else { + config.ErrorReporting = m.Value.Decoded.(bool) + } +} diff --git a/backend/internal/entity/setting/filters.go b/backend/internal/entity/setting/filters.go new file mode 100644 index 00000000..c9e92416 --- /dev/null +++ b/backend/internal/entity/setting/filters.go @@ -0,0 +1,25 @@ +package setting + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/setting/methods.go b/backend/internal/entity/setting/methods.go new file mode 100644 index 00000000..51b75177 --- /dev/null +++ b/backend/internal/entity/setting/methods.go @@ -0,0 +1,127 @@ +package setting + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a setting by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByName finds a setting by name +func GetByName(name string) (Model, error) { + var m Model + err := m.LoadByName(name) + return m, err +} + +// Create will Create a Setting from this model +func Create(setting *Model) (int, error) { + if setting.ID != 0 { + return 0, goerrors.New("Cannot create setting when model already has an ID") + } + + setting.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + value + ) VALUES ( + :created_on, + :modified_on, + :name, + :value + )`, setting) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Setting from this model +func Update(setting *Model) error { + if setting.ID == 0 { + return goerrors.New("Cannot update setting when model doesn't have an ID") + } + + setting.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + value = :value + WHERE id = :id`, setting) + + return err +} + +// List will return a list of settings +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%+v", queryErr) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%+v", err) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/setting/model.go b/backend/internal/entity/setting/model.go new file mode 100644 index 00000000..e66d7276 --- /dev/null +++ b/backend/internal/entity/setting/model.go @@ -0,0 +1,70 @@ +package setting + +import ( + "fmt" + "strings" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "setting" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Description string `json:"description" db:"description" filter:"description,string"` + Value types.JSONB `json:"value" db:"value"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE `id` = ? LIMIT 1", tableName) + params := []interface{}{id} + return m.getByQuery(query, params) +} + +// LoadByName will load from a Name +func (m *Model) LoadByName(name string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE LOWER(`name`) = ? LIMIT 1", tableName) + params := []interface{}{strings.TrimSpace(strings.ToLower(name))} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + // Reapply settings + if err == nil { + ApplySettings() + } + + return err +} diff --git a/backend/internal/entity/setting/structs.go b/backend/internal/entity/setting/structs.go new file mode 100644 index 00000000..ba9851bd --- /dev/null +++ b/backend/internal/entity/setting/structs.go @@ -0,0 +1,15 @@ +package setting + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for settings list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/stream/filters.go b/backend/internal/entity/stream/filters.go new file mode 100644 index 00000000..bf4c8832 --- /dev/null +++ b/backend/internal/entity/stream/filters.go @@ -0,0 +1,25 @@ +package stream + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/stream/methods.go b/backend/internal/entity/stream/methods.go new file mode 100644 index 00000000..02ddd4d6 --- /dev/null +++ b/backend/internal/entity/stream/methods.go @@ -0,0 +1,135 @@ +package stream + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a auth by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a Auth from this model +func Create(host *Model) (int, error) { + if host.ID != 0 { + return 0, goerrors.New("Cannot create stream when model already has an ID") + } + + host.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + provider, + name, + domain_names, + expires_on, + meta, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :provider, + :name, + :domain_names, + :expires_on, + :meta, + :is_deleted + )`, host) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Host from this model +func Update(host *Model) error { + if host.ID == 0 { + return goerrors.New("Cannot update stream when model doesn't have an ID") + } + + host.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + provider = :provider, + name = :name, + domain_names = :domain_names, + expires_on = :expires_on, + meta = :meta, + is_deleted = :is_deleted + WHERE id = :id`, host) + + return err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/stream/model.go b/backend/internal/entity/stream/model.go new file mode 100644 index 00000000..60f47f75 --- /dev/null +++ b/backend/internal/entity/stream/model.go @@ -0,0 +1,75 @@ +package stream + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "stream" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + ExpiresOn types.DBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Provider string `json:"provider" db:"provider" filter:"provider,string"` + Name string `json:"name" db:"name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + Meta types.JSONB `json:"-" db:"meta"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a host as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/stream/structs.go b/backend/internal/entity/stream/structs.go new file mode 100644 index 00000000..ec732c13 --- /dev/null +++ b/backend/internal/entity/stream/structs.go @@ -0,0 +1,15 @@ +package stream + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/user/capabilities.go b/backend/internal/entity/user/capabilities.go new file mode 100644 index 00000000..704e7b6f --- /dev/null +++ b/backend/internal/entity/user/capabilities.go @@ -0,0 +1,40 @@ +package user + +const ( + // CapabilityFullAdmin can do anything + CapabilityFullAdmin = "full-admin" + // CapabilityAccessListsView access lists view + CapabilityAccessListsView = "access-lists.view" + // CapabilityAccessListsManage access lists manage + CapabilityAccessListsManage = "access-lists.manage" + // CapabilityAuditLogView audit log view + CapabilityAuditLogView = "audit-log.view" + // CapabilityCertificatesView certificates view + CapabilityCertificatesView = "certificates.view" + // CapabilityCertificatesManage certificates manage + CapabilityCertificatesManage = "certificates.manage" + // CapabilityCertificateAuthoritiesView certificate authorities view + CapabilityCertificateAuthoritiesView = "certificate-authorities.view" + // CapabilityCertificateAuthoritiesManage certificate authorities manage + CapabilityCertificateAuthoritiesManage = "certificate-authorities.manage" + // CapabilityDNSProvidersView dns providers view + CapabilityDNSProvidersView = "dns-providers.view" + // CapabilityDNSProvidersManage dns providers manage + CapabilityDNSProvidersManage = "dns-providers.manage" + // CapabilityHostsView hosts view + CapabilityHostsView = "hosts.view" + // CapabilityHostsManage hosts manage + CapabilityHostsManage = "hosts.manage" + // CapabilityHostTemplatesView host-templates view + CapabilityHostTemplatesView = "host-templates.view" + // CapabilityHostTemplatesManage host-templates manage + CapabilityHostTemplatesManage = "host-templates.manage" + // CapabilitySettingsManage settings manage + CapabilitySettingsManage = "settings.manage" + // CapabilityStreamsView streams view + CapabilityStreamsView = "streams.view" + // CapabilityStreamsManage streams manage + CapabilityStreamsManage = "streams.manage" + // CapabilityUsersManage users manage + CapabilityUsersManage = "users.manage" +) diff --git a/backend/internal/entity/user/filters.go b/backend/internal/entity/user/filters.go new file mode 100644 index 00000000..bc38ef69 --- /dev/null +++ b/backend/internal/entity/user/filters.go @@ -0,0 +1,25 @@ +package user + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go new file mode 100644 index 00000000..42b52476 --- /dev/null +++ b/backend/internal/entity/user/methods.go @@ -0,0 +1,229 @@ +package user + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a user by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByEmail finds a user by email +func GetByEmail(email string) (Model, error) { + var m Model + err := m.LoadByEmail(email) + return m, err +} + +// Create will create a User from given model +func Create(user *Model) (int, error) { + // We need to ensure that a user can't be created with the same email + // as an existing non-deleted user. Usually you would do this with the + // database schema, but it's a bit more complex because of the is_deleted field. + + if user.ID != 0 { + return 0, goerrors.New("Cannot create user when model already has an ID") + } + + // Check if an existing user with this email exists + _, err := GetByEmail(user.Email) + if err == nil { + return 0, errors.ErrDuplicateEmailUser + } + + user.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + nickname, + email, + is_disabled + ) VALUES ( + :created_on, + :modified_on, + :name, + :nickname, + :email, + :is_disabled + )`, user) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a User from this model +func Update(user *Model) error { + if user.ID == 0 { + return goerrors.New("Cannot update user when model doesn't have an ID") + } + + // Check that the email address isn't associated with another user + if existingUser, _ := GetByEmail(user.Email); existingUser.ID != 0 && existingUser.ID != user.ID { + return errors.ErrDuplicateEmailUser + } + + user.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + nickname = :nickname, + email = :email, + is_disabled = :is_disabled, + is_deleted = :is_deleted + WHERE id = :id`, user) + + return err +} + +// IsEnabled is used by middleware to ensure the user is still enabled +// returns (userExist, isEnabled) +func IsEnabled(userID int) (bool, bool) { + // nolint: gosec + query := `SELECT is_disabled FROM ` + fmt.Sprintf("`%s`", tableName) + ` WHERE id = ? AND is_deleted = ?` + disabled := true + db := database.GetInstance() + err := db.QueryRowx(query, userID, 0).Scan(&disabled) + + if err == sql.ErrNoRows { + return false, false + } else if err != nil { + logger.Error("QueryError", err) + } + + return true, !disabled +} + +// List will return a list of users +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + /* + filters = append(filters, model.Filter{ + Field: "is_system", + Modifier: "equals", + Value: []string{"0"}, + }) + */ + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("Query: %s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("Query: %s -- %+v", query, params) + return result, err + } + + for idx := range items { + items[idx].generateGravatar() + } + + if expand != nil { + for idx := range items { + expandErr := items[idx].Expand(expand) + if expandErr != nil { + logger.Error("UsersExpansionError", expandErr) + } + } + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} + +// DeleteAll will do just that, and should only be used for testing purposes. +func DeleteAll() error { + db := database.GetInstance() + _, err := db.Exec(fmt.Sprintf("DELETE FROM `%s`", tableName)) + return err +} + +// GetCapabilities gets capabilities for a user +func GetCapabilities(userID int) ([]string, error) { + var capabilities []string + db := database.GetInstance() + if db == nil { + return []string{}, errors.ErrDatabaseUnavailable + } + + query := `SELECT c.name FROM "user_has_capability" h + INNER JOIN "capability" c ON c.id = h.capability_id + WHERE h.user_id = ?` + + rows, err := db.Query(query, userID) + if err != nil && err != sql.ErrNoRows { + logger.Debug("QUERY: %v -- %v", query, userID) + return []string{}, err + } + + // nolint: errcheck + defer rows.Close() + + for rows.Next() { + var name string + err := rows.Scan(&name) + if err != nil { + return []string{}, err + } + + capabilities = append(capabilities, name) + } + + return capabilities, nil +} diff --git a/backend/internal/entity/user/model.go b/backend/internal/entity/user/model.go new file mode 100644 index 00000000..bf2f4c84 --- /dev/null +++ b/backend/internal/entity/user/model.go @@ -0,0 +1,191 @@ +package user + +import ( + goerrors "errors" + "fmt" + "strings" + "time" + + "npm/internal/database" + "npm/internal/entity/auth" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/types" + "npm/internal/util" + + "github.com/drexedam/gravatar" +) + +const ( + tableName = "user" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Nickname string `json:"nickname" db:"nickname" filter:"nickname,string"` + Email string `json:"email" db:"email" filter:"email,email"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + GravatarURL string `json:"gravatar_url"` + IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` + IsSystem bool `json:"is_system,omitempty" db:"is_system"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions + Auth *auth.Model `json:"auth,omitempty" db:"-"` + Capabilities []string `json:"capabilities,omitempty"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + err := database.GetByQuery(m, query, params) + m.generateGravatar() + return err +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, false} + return m.getByQuery(query, params) +} + +// LoadByEmail will load from an Email +func (m *Model) LoadByEmail(email string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE email = ? AND is_deleted = ? AND is_system = ? LIMIT 1", tableName) + params := []interface{}{strings.TrimSpace(strings.ToLower(email)), false, false} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d + m.generateGravatar() +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + // Ensure email is nice + m.Email = strings.TrimSpace(strings.ToLower(m.Email)) + + if m.IsSystem { + return errors.ErrSystemUserReadonly + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a user as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// SetPermissions will wipe out any existing permissions and add new ones for this user +func (m *Model) SetPermissions(permissions []string) error { + if m.ID == 0 { + return fmt.Errorf("Cannot set permissions without first saving the User") + } + + db := database.GetInstance() + + // Wipe out previous permissions + query := `DELETE FROM "user_has_capability" WHERE "user_id" = ?` + if _, err := db.Exec(query, m.ID); err != nil { + logger.Debug("QUERY: %v -- %v", query, m.ID) + return err + } + + if len(permissions) > 0 { + // Add new permissions + for _, permission := range permissions { + query = `INSERT INTO "user_has_capability" ( + "user_id", "capability_id" + ) VALUES ( + ?, + (SELECT id FROM capability WHERE name = ?) + )` + + _, err := db.Exec(query, m.ID, permission) + if err != nil { + logger.Debug("QUERY: %v -- %v -- %v", query, m.ID, permission) + return err + } + } + } + + return nil +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + if util.SliceContainsItem(items, "capabilities") && m.ID > 0 { + m.Capabilities, err = GetCapabilities(m.ID) + } + + return err +} + +func (m *Model) generateGravatar() { + m.GravatarURL = gravatar.New(m.Email). + Size(128). + Default(gravatar.MysteryMan). + Rating(gravatar.Pg). + AvatarURL() +} + +// SaveCapabilities will save the capabilities of the user. +func (m *Model) SaveCapabilities() error { + // m.Capabilities + if m.ID == 0 { + return fmt.Errorf("Cannot save capabilities on unsaved user") + } + + // there must be at least 1 capability + if len(m.Capabilities) == 0 { + return goerrors.New("At least 1 capability required for a user") + } + + db := database.GetInstance() + + // Get a full list of capabilities + var capabilities []string + query := `SELECT "name" from "capability"` + err := db.Select(&capabilities, query) + if err != nil { + return err + } + + // Check that the capabilities defined exist in the db + for _, cap := range m.Capabilities { + found := false + for _, a := range capabilities { + if a == cap { + found = true + } + } + if !found { + return fmt.Errorf("Capability `%s` is not valid", cap) + } + } + + return m.SetPermissions(m.Capabilities) +} diff --git a/backend/internal/entity/user/structs.go b/backend/internal/entity/user/structs.go new file mode 100644 index 00000000..f9f4490e --- /dev/null +++ b/backend/internal/entity/user/structs.go @@ -0,0 +1,15 @@ +package user + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/errors/errors.go b/backend/internal/errors/errors.go new file mode 100644 index 00000000..39735a1d --- /dev/null +++ b/backend/internal/errors/errors.go @@ -0,0 +1,16 @@ +package errors + +import "errors" + +// All error messages used by the service package to report +// problems back to calling clients +var ( + ErrDatabaseUnavailable = errors.New("database-unavailable") + ErrDuplicateEmailUser = errors.New("email-already-exists") + ErrInvalidLogin = errors.New("invalid-login-credentials") + ErrUserDisabled = errors.New("user-disabled") + ErrSystemUserReadonly = errors.New("cannot-save-system-users") + ErrValidationFailed = errors.New("request-failed-validation") + ErrCurrentPasswordInvalid = errors.New("current-password-invalid") + ErrCABundleDoesNotExist = errors.New("ca-bundle-does-not-exist") +) diff --git a/backend/internal/host.js b/backend/internal/host.js deleted file mode 100644 index 58e1d09a..00000000 --- a/backend/internal/host.js +++ /dev/null @@ -1,235 +0,0 @@ -const _ = require('lodash'); -const proxyHostModel = require('../models/proxy_host'); -const redirectionHostModel = require('../models/redirection_host'); -const deadHostModel = require('../models/dead_host'); - -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: function (data, existing_data) { - existing_data = existing_data === undefined ? {} : existing_data; - - let combined_data = _.assign({}, existing_data, data); - - if (!combined_data.certificate_id) { - combined_data.ssl_forced = false; - combined_data.http2_support = false; - } - - if (!combined_data.ssl_forced) { - combined_data.hsts_enabled = false; - } - - if (!combined_data.hsts_enabled) { - combined_data.hsts_subdomains = false; - } - - return combined_data; - }, - - /** - * used by the getAll functions of hosts, this removes the certificate meta if present - * - * @param {Array} rows - * @returns {Array} - */ - cleanAllRowsCertificateMeta: function (rows) { - rows.map(function (row, idx) { - if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) { - rows[idx].certificate.meta = {}; - } - }); - - return rows; - }, - - /** - * used by the get/update functions of hosts, this removes the certificate meta if present - * - * @param {Object} row - * @returns {Object} - */ - cleanRowCertificateMeta: function (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 domain_names array. - * This is used by the certificates to temporarily disable any host that is using the domain - * - * @param {Array} domain_names - * @returns {Promise} - */ - getHostsWithDomains: function (domain_names) { - let promises = [ - proxyHostModel - .query() - .where('is_deleted', 0), - redirectionHostModel - .query() - .where('is_deleted', 0), - deadHostModel - .query() - .where('is_deleted', 0) - ]; - - return Promise.all(promises) - .then((promises_results) => { - let response_object = { - total_count: 0, - dead_hosts: [], - proxy_hosts: [], - redirection_hosts: [] - }; - - if (promises_results[0]) { - // Proxy Hosts - response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names); - response_object.total_count += response_object.proxy_hosts.length; - } - - if (promises_results[1]) { - // Redirection Hosts - response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names); - response_object.total_count += response_object.redirection_hosts.length; - } - - if (promises_results[2]) { - // Dead Hosts - response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names); - response_object.total_count += response_object.dead_hosts.length; - } - - return response_object; - }); - }, - - /** - * 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: function (hostname, ignore_type, ignore_id) { - let promises = [ - proxyHostModel - .query() - .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%'), - redirectionHostModel - .query() - .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%'), - deadHostModel - .query() - .where('is_deleted', 0) - .andWhere('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} existing_rows - * @param {Integer} [ignore_id] - * @returns {Boolean} - */ - _checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) { - let is_taken = false; - - if (existing_rows && existing_rows.length) { - existing_rows.map(function (existing_row) { - existing_row.domain_names.map(function (existing_hostname) { - // Does this domain match? - if (existing_hostname.toLowerCase() === hostname.toLowerCase()) { - if (!ignore_id || ignore_id !== existing_row.id) { - is_taken = true; - } - } - }); - }); - } - - return is_taken; - }, - - /** - * Private call only - * - * @param {Array} hosts - * @param {Array} domain_names - * @returns {Array} - */ - _getHostsWithDomains: function (hosts, domain_names) { - let response = []; - - if (hosts && hosts.length) { - hosts.map(function (host) { - let host_matches = false; - - domain_names.map(function (domain_name) { - host.domain_names.map(function (host_domain_name) { - if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) { - host_matches = true; - } - }); - }); - - if (host_matches) { - response.push(host); - } - }); - } - - return response; - } - -}; - -module.exports = internalHost; diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js deleted file mode 100644 index 40e63ea4..00000000 --- a/backend/internal/ip_ranges.js +++ /dev/null @@ -1,150 +0,0 @@ -const https = require('https'); -const fs = require('fs'); -const logger = require('../logger').ip_ranges; -const error = require('../lib/error'); -const internalNginx = require('./nginx'); -const { Liquid } = require('liquidjs'); - -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) => { - return new Promise((resolve, reject) => { - logger.info('Fetching ' + url); - return https.get(url, (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) => { - let 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); - } - }); - } - - if (data && typeof data.ipv6_prefixes !== 'undefined') { - data.ipv6_prefixes.map((item) => { - if (item.service === 'CLOUDFRONT') { - ip_ranges.push(item.ipv6_prefix); - } - }); - } - }) - .then(() => { - return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL); - }) - .then((cloudfare_data) => { - let 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) => { - let items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line)); - ip_ranges = [... ip_ranges, ... items]; - }) - .then(() => { - let clean_ip_ranges = []; - ip_ranges.map((range) => { - if (range) { - clean_ip_ranges.push(range); - } - }); - - 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.error(err.message); - internalIpRanges.interval_processing = false; - }); - } - }, - - /** - * @param {Array} ip_ranges - * @returns {Promise} - */ - generateConfig: (ip_ranges) => { - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = '/etc/nginx/conf.d/include/ip_ranges.conf'; - try { - template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.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 error.ConfigurationError(err.message)); - }); - }); - } -}; - -module.exports = internalIpRanges; diff --git a/backend/internal/jwt/jwt.go b/backend/internal/jwt/jwt.go new file mode 100644 index 00000000..9dcb6c7b --- /dev/null +++ b/backend/internal/jwt/jwt.go @@ -0,0 +1,60 @@ +package jwt + +import ( + "fmt" + "time" + + "npm/internal/entity/user" + "npm/internal/logger" + + "github.com/dgrijalva/jwt-go" +) + +// UserJWTClaims is the structure of a JWT for a User +type UserJWTClaims struct { + UserID int `json:"uid"` + Roles []string `json:"roles"` + jwt.StandardClaims +} + +// GeneratedResponse is the response of a generated token, usually used in http response +type GeneratedResponse struct { + Expires int64 `json:"expires"` + Token string `json:"token"` +} + +// Generate will create a JWT +func Generate(userObj *user.Model) (GeneratedResponse, error) { + var response GeneratedResponse + + key, _ := GetPrivateKey() + expires := time.Now().AddDate(0, 0, 1) // 1 day + + // Create the Claims + claims := UserJWTClaims{ + userObj.ID, + []string{"user"}, + jwt.StandardClaims{ + IssuedAt: time.Now().Unix(), + ExpiresAt: expires.Unix(), + Issuer: "api", + }, + } + + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + var err error + token.Signature, err = token.SignedString(key) + if err != nil { + logger.Error("JWTError", fmt.Errorf("Error signing token: %v", err)) + return response, err + } + + response = GeneratedResponse{ + Expires: expires.Unix(), + Token: token.Signature, + } + + return response, nil +} diff --git a/backend/internal/jwt/keys.go b/backend/internal/jwt/keys.go new file mode 100644 index 00000000..e48c334d --- /dev/null +++ b/backend/internal/jwt/keys.go @@ -0,0 +1,86 @@ +package jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + "npm/internal/config" +) + +var ( + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey +) + +// GetPrivateKey will load the key from config package and return a usable object +// It should only load from file once per program execution +func GetPrivateKey() (*rsa.PrivateKey, error) { + if privateKey == nil { + var blankKey *rsa.PrivateKey + + if config.PrivateKey == "" { + return blankKey, errors.New("Could not get Private Key from configuration") + } + + var err error + privateKey, err = LoadPemPrivateKey(config.PrivateKey) + if err != nil { + return blankKey, err + } + } + + pub, pubErr := GetPublicKey() + if pubErr != nil { + return privateKey, pubErr + } + + privateKey.PublicKey = *pub + + return privateKey, pubErr +} + +// GetPublicKey will load the key from config package and return a usable object +// It should only load once per program execution +func GetPublicKey() (*rsa.PublicKey, error) { + if publicKey == nil { + var blankKey *rsa.PublicKey + + if config.PublicKey == "" { + return blankKey, errors.New("Could not get Public Key filename, check environment variables") + } + + var err error + publicKey, err = LoadPemPublicKey(config.PublicKey) + if err != nil { + return blankKey, err + } + } + + return publicKey, nil +} + +// LoadPemPrivateKey reads a key from a PEM encoded string and returns a private key +func LoadPemPrivateKey(content string) (*rsa.PrivateKey, error) { + var key *rsa.PrivateKey + data, _ := pem.Decode([]byte(content)) + var err error + key, err = x509.ParsePKCS1PrivateKey(data.Bytes) + if err != nil { + return key, err + } + return key, nil +} + +// LoadPemPublicKey reads a key from a PEM encoded string and returns a public key +func LoadPemPublicKey(content string) (*rsa.PublicKey, error) { + var key *rsa.PublicKey + data, _ := pem.Decode([]byte(content)) + publicKeyFileImported, err := x509.ParsePKCS1PublicKey(data.Bytes) + if err != nil { + return key, err + } + + return publicKeyFileImported, nil +} diff --git a/backend/internal/logger/config.go b/backend/internal/logger/config.go new file mode 100644 index 00000000..c0f8a35a --- /dev/null +++ b/backend/internal/logger/config.go @@ -0,0 +1,40 @@ +package logger + +import "github.com/getsentry/sentry-go" + +// Level type +type Level int + +// Log level definitions +const ( + // DebugLevel usually only enabled when debugging. Very verbose logging. + DebugLevel Level = 10 + // InfoLevel general operational entries about what's going on inside the application. + InfoLevel Level = 20 + // WarnLevel non-critical entries that deserve eyes. + WarnLevel Level = 30 + // ErrorLevel used for errors that should definitely be noted. + ErrorLevel Level = 40 +) + +// Config options for the logger. +type Config struct { + LogThreshold Level + Formatter string + SentryConfig sentry.ClientOptions +} + +// Interface for a logger +type Interface interface { + GetLogLevel() Level + Debug(format string, args ...interface{}) + Info(format string, args ...interface{}) + Warn(format string, args ...interface{}) + Error(errorClass string, err error, args ...interface{}) + Errorf(errorClass, format string, err error, args ...interface{}) +} + +// ConfigurableLogger is an interface for a logger that can be configured +type ConfigurableLogger interface { + Configure(c *Config) error +} diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go new file mode 100644 index 00000000..f82145ee --- /dev/null +++ b/backend/internal/logger/logger.go @@ -0,0 +1,242 @@ +package logger + +import ( + "encoding/json" + "fmt" + stdlog "log" + "os" + "runtime/debug" + "sync" + "time" + + "github.com/fatih/color" + "github.com/getsentry/sentry-go" +) + +var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color + +// Log message structure. +type Log struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + Pid int `json:"pid"` + Summary string `json:"summary,omitempty"` + Caller string `json:"caller,omitempty"` + StackTrace []string `json:"stack_trace,omitempty"` +} + +// Logger instance +type Logger struct { + Config + mux sync.Mutex +} + +// global logging configuration. +var logger = NewLogger() + +// NewLogger creates a new logger instance +func NewLogger() *Logger { + color.NoColor = false + colorReset = color.New(color.Reset) + colorGray = color.New(color.FgWhite) + colorYellow = color.New(color.Bold, color.FgYellow) + colorBlue = color.New(color.Bold, color.FgBlue) + colorRed = color.New(color.Bold, color.FgRed) + colorMagenta = color.New(color.Bold, color.FgMagenta) + colorBlack = color.New(color.Bold, color.FgBlack) + colorWhite = color.New(color.Bold, color.FgWhite) + + return &Logger{ + Config: NewConfig(), + } +} + +// NewConfig returns the default config +func NewConfig() Config { + return Config{ + LogThreshold: InfoLevel, + Formatter: "json", + } +} + +// Configure logger and will return error if missing required fields. +func Configure(c *Config) error { + return logger.Configure(c) +} + +// GetLogLevel currently configured +func GetLogLevel() Level { + return logger.GetLogLevel() +} + +// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf. +func Debug(format string, args ...interface{}) { + logger.Debug(format, args...) +} + +// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf. +func Info(format string, args ...interface{}) { + logger.Info(format, args...) +} + +// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf. +func Warn(format string, args ...interface{}) { + logger.Warn(format, args...) +} + +// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged. +// Attempts to log to bugsang. +func Error(errorClass string, err error) { + logger.Error(errorClass, err) +} + +// Configure logger and will return error if missing required fields. +func (l *Logger) Configure(c *Config) error { + // ensure updates to the config are atomic + l.mux.Lock() + defer l.mux.Unlock() + + if c == nil { + return fmt.Errorf("a non nil Config is mandatory") + } + + if err := c.LogThreshold.validate(); err != nil { + return err + } + + l.LogThreshold = c.LogThreshold + l.Formatter = c.Formatter + l.SentryConfig = c.SentryConfig + + if c.SentryConfig.Dsn != "" { + if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil { + fmt.Printf("Sentry initialization failed: %v\n", sentryErr) + } + } + + stdlog.SetFlags(0) // this removes timestamp prefixes from logs + return nil +} + +// validate the log level is in the accepted list. +func (l Level) validate() error { + switch l { + case DebugLevel, InfoLevel, WarnLevel, ErrorLevel: + return nil + default: + return fmt.Errorf("invalid \"Level\" %d", l) + } +} + +var logLevels = map[Level]string{ + DebugLevel: "DEBUG", + InfoLevel: "INFO", + WarnLevel: "WARN", + ErrorLevel: "ERROR", +} + +func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) { + if logLevel < l.LogThreshold { + return + } + + errorClass := "" + if logLevel == ErrorLevel { + // First arg is the errorClass + errorClass = args[0].(string) + if len(args) > 1 { + args = args[1:] + } else { + args = []interface{}{} + } + } + + stringMessage := fmt.Sprintf(format, args...) + + if l.Formatter == "json" { + // JSON Log Format + jsonLog, _ := json.Marshal( + Log{ + Timestamp: time.Now().Format(time.RFC3339Nano), + Level: logLevels[logLevel], + Message: stringMessage, + Pid: os.Getpid(), + }, + ) + + stdlog.Println(string(jsonLog)) + } else { + // Nice Log Format + var colorLevel *color.Color + switch logLevel { + case DebugLevel: + colorLevel = colorMagenta + case InfoLevel: + colorLevel = colorBlue + case WarnLevel: + colorLevel = colorYellow + case ErrorLevel: + colorLevel = colorRed + stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage) + } + + t := time.Now() + stdlog.Println( + colorBlack.Sprint("["), + colorWhite.Sprint(t.Format("2006-01-02 15:04:05")), + colorBlack.Sprint("] "), + colorLevel.Sprintf("%-8v", logLevels[logLevel]), + colorGray.Sprint(stringMessage), + colorReset.Sprint(""), + ) + + if logLevel == ErrorLevel && l.LogThreshold == DebugLevel { + // Print a stack trace too + debug.PrintStack() + } + } +} + +// GetLogLevel currently configured +func (l *Logger) GetLogLevel() Level { + return l.LogThreshold +} + +// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Debug(format string, args ...interface{}) { + l.logLevel(DebugLevel, format, args...) +} + +// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Info(format string, args ...interface{}) { + l.logLevel(InfoLevel, format, args...) +} + +// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Warn(format string, args ...interface{}) { + l.logLevel(WarnLevel, format, args...) +} + +// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged. +// Attempts to log to bugsang. +func (l *Logger) Error(errorClass string, err error) { + l.logLevel(ErrorLevel, err.Error(), errorClass) + l.notifySentry(errorClass, err) +} + +func (l *Logger) notifySentry(errorClass string, err error) { + if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" { + + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelError) + scope.SetTag("service", "backend") + scope.SetTag("error_class", errorClass) + }) + + sentry.CaptureException(err) + // Since sentry emits events in the background we need to make sure + // they are sent before we shut down + sentry.Flush(time.Second * 5) + } +} diff --git a/backend/internal/logger/logger_test.go b/backend/internal/logger/logger_test.go new file mode 100644 index 00000000..e0019696 --- /dev/null +++ b/backend/internal/logger/logger_test.go @@ -0,0 +1,168 @@ +package logger + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "testing" + + "github.com/getsentry/sentry-go" + "github.com/stretchr/testify/assert" +) + +func TestGetLogLevel(t *testing.T) { + assert.Equal(t, InfoLevel, GetLogLevel()) +} + +func TestThreshold(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Debug("this should not display") + assert.Empty(t, buf.String()) + + Info("this should display") + assert.NotEmpty(t, buf.String()) + + Error("ErrorClass", errors.New("this should display")) + assert.NotEmpty(t, buf.String()) +} + +func TestDebug(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: DebugLevel, + })) + + Debug("This is a %s message", "test") + assert.Contains(t, buf.String(), "DEBUG") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestInfo(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Info("This is a %s message", "test") + assert.Contains(t, buf.String(), "INFO") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestWarn(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Warn("This is a %s message", "test") + assert.Contains(t, buf.String(), "WARN") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestError(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: ErrorLevel, + })) + + Error("TestErrorClass", fmt.Errorf("this is a %s error", "test")) + assert.Contains(t, buf.String(), "ERROR") + assert.Contains(t, buf.String(), "this is a test error") +} + +func TestConfigure(t *testing.T) { + type args struct { + c *Config + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "configure", + args: args{ + &Config{ + LogThreshold: InfoLevel, + SentryConfig: sentry.ClientOptions{}, + }, + }, + wantErr: false, + }, + { + name: "invalid log level", + args: args{ + &Config{ + SentryConfig: sentry.ClientOptions{}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if err := Configure(tt.args.c); (err != nil) != tt.wantErr { + t.Errorf("Configure() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func BenchmarkLogLevelBelowThreshold(b *testing.B) { + l := NewLogger() + + log.SetOutput(ioutil.Discard) + defer func() { + log.SetOutput(os.Stderr) + }() + + for i := 0; i < b.N; i++ { + l.logLevel(DebugLevel, "benchmark %d", i) + } +} + +func BenchmarkLogLevelAboveThreshold(b *testing.B) { + l := NewLogger() + + log.SetOutput(ioutil.Discard) + defer func() { + log.SetOutput(os.Stderr) + }() + + for i := 0; i < b.N; i++ { + l.logLevel(InfoLevel, "benchmark %d", i) + } +} diff --git a/backend/internal/model/filter.go b/backend/internal/model/filter.go new file mode 100644 index 00000000..8b88b70c --- /dev/null +++ b/backend/internal/model/filter.go @@ -0,0 +1,8 @@ +package model + +// Filter is the structure of a field/modifier/value item +type Filter struct { + Field string `json:"field"` + Modifier string `json:"modifier"` + Value []string `json:"value"` +} diff --git a/backend/internal/model/pageinfo.go b/backend/internal/model/pageinfo.go new file mode 100644 index 00000000..3f1fd26f --- /dev/null +++ b/backend/internal/model/pageinfo.go @@ -0,0 +1,22 @@ +package model + +import ( + "time" +) + +// PageInfo is the model used by Api Handlers and passed on to other parts +// of the application +type PageInfo struct { + FromDate time.Time `json:"from_date"` + ToDate time.Time `json:"to_date"` + Sort []Sort `json:"sort"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Expand []string `json:"expand"` +} + +// Sort holds the sorting data +type Sort struct { + Field string `json:"field"` + Direction string `json:"direction"` +} diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js deleted file mode 100644 index 52bdd66d..00000000 --- a/backend/internal/nginx.js +++ /dev/null @@ -1,435 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); -const { Liquid } = require('liquidjs'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; - -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. - return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all - }) - .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) - - let valid_lines = []; - let err_lines = err.message.split('\n'); - err_lines.map(function (line) { - if (line.indexOf('/var/log/nginx/error.log') === -1) { - valid_lines.push(line); - } - }); - - if (debug_mode) { - logger.error('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(() => { - return internalNginx.deleteConfig(host_type, host, true); - }); - }); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - return combined_meta; - }); - }, - - /** - * @returns {Promise} - */ - test: () => { - if (debug_mode) { - logger.info('Testing Nginx configuration'); - } - - return utils.exec('/usr/sbin/nginx -t -g "error_log off;"'); - }, - - /** - * @returns {Promise} - */ - reload: () => { - return internalNginx.test() - .then(() => { - logger.info('Reloading Nginx'); - return utils.exec('/usr/sbin/nginx -s reload'); - }); - }, - - /** - * @param {String} host_type - * @param {Integer} host_id - * @returns {String} - */ - getConfigName: (host_type, host_id) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - if (host_type === 'default') { - return '/data/nginx/default_host/site.conf'; - } - - return '/data/nginx/' + host_type + '/' + host_id + '.conf'; - }, - - /** - * Generates custom locations - * @param {Object} host - * @returns {Promise} - */ - renderLocations: (host) => { - - //logger.info('host = ' + JSON.stringify(host, null, 2)); - return new Promise((resolve, reject) => { - let template; - - try { - template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - let renderer = new Liquid({ - root: __dirname + '/../templates/' - }); - let renderedLocations = ''; - - const locationRendering = async () => { - for (let i = 0; i < host.locations.length; i++) { - let 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('/')}`; - } - - //logger.info('locationCopy = ' + JSON.stringify(locationCopy, null, 2)); - - // eslint-disable-next-line - renderedLocations += await renderer.parseAndRender(template, locationCopy); - } - - }; - - locationRendering().then(() => resolve(renderedLocations)); - - }); - }, - - /** - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - generateConfig: (host_type, host) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - if (debug_mode) { - logger.info('Generating ' + host_type + ' Config:', host); - } - - // logger.info('host = ' + JSON.stringify(host, null, 2)); - - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = internalNginx.getConfigName(host_type, host.id); - - try { - template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - let locationsPromise; - let origLocations; - - // Manipulate the data a bit before sending it to the template - if (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); - } - } - - 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'}); - - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } - - // Restore locations array - host.locations = origLocations; - - resolve(true); - }) - .catch((err) => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.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) => { - if (debug_mode) { - logger.info('Generating LetsEncrypt Request Config:', certificate); - } - - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; - - try { - template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - certificate.ipv6 = internalNginx.ipv6Enabled(); - - renderEngine - .parseAndRender(template, certificate) - .then((config_text) => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } - - resolve(true); - }) - .catch((err) => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); - }); - }); - }, - - /** - * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` - * - * @param {Object} certificate - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - deleteLetsEncryptRequestConfig: (certificate, throw_errors) => { - return new Promise((resolve, reject) => { - try { - let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; - - if (debug_mode) { - logger.warn('Deleting nginx config: ' + config_file); - } - - fs.unlinkSync(config_file); - } catch (err) { - if (debug_mode) { - logger.warn('Could not delete config:', err.message); - } - - if (throw_errors) { - reject(err); - } - } - - resolve(); - }); - }, - - /** - * @param {String} host_type - * @param {Object} [host] - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - deleteConfig: (host_type, host, throw_errors) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - return new Promise((resolve, reject) => { - try { - let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id); - - if (debug_mode) { - logger.warn('Deleting nginx config: ' + config_file); - } - - fs.unlinkSync(config_file); - } catch (err) { - if (debug_mode) { - logger.warn('Could not delete config:', err.message); - } - - if (throw_errors) { - reject(err); - } - } - - resolve(); - }); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @returns {Promise} - */ - bulkGenerateConfigs: (host_type, hosts) => { - let promises = []; - hosts.map(function (host) { - promises.push(internalNginx.generateConfig(host_type, host)); - }); - - return Promise.all(promises); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - bulkDeleteConfigs: (host_type, hosts, throw_errors) => { - let promises = []; - hosts.map(function (host) { - promises.push(internalNginx.deleteConfig(host_type, host, throw_errors)); - }); - - return Promise.all(promises); - }, - - /** - * @param {string} config - * @returns {boolean} - */ - advancedConfigHasDefaultLocation: function (config) { - return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); - }, - - /** - * @returns {boolean} - */ - ipv6Enabled: function () { - 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; - } -}; - -module.exports = internalNginx; diff --git a/backend/internal/nginx/templates.go b/backend/internal/nginx/templates.go new file mode 100644 index 00000000..3c87b2b2 --- /dev/null +++ b/backend/internal/nginx/templates.go @@ -0,0 +1,31 @@ +package nginx + +import ( + "io/fs" + "io/ioutil" + + "npm/embed" + + "github.com/aymerick/raymond" +) + +// WriteTemplate will load, parse and write a template file +func WriteTemplate(templateName, outputFilename string, data map[string]interface{}) error { + // get template file content + subFs, _ := fs.Sub(embed.NginxFiles, "nginx") + template, err := fs.ReadFile(subFs, templateName) + + if err != nil { + return err + } + + // Render + parsedFile, err := raymond.Render(string(template), data) + if err != nil { + return err + } + + // Write it + // nolint: gosec + return ioutil.WriteFile(outputFilename, []byte(parsedFile), 0644) +} diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js deleted file mode 100644 index 09b8bca5..00000000 --- a/backend/internal/proxy-host.js +++ /dev/null @@ -1,466 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const proxyHostModel = require('../models/proxy_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalProxyHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('proxy_hosts:create', data) - .then(() => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return proxyHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalProxyHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - 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 - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'proxy-host', - 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 create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('proxy_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalProxyHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return proxyHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalProxyHost.get(access, { - id: data.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; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(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) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('proxy_hosts:get', data.id) - .then((access_data) => { - let query = proxyHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,access_list,access_list.[clients,items],certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @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) { - throw new error.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) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.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) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.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: (access, expand, search_query) => { - return access.can('proxy_hosts:list') - .then((access_data) => { - let query = proxyHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,access_list,certificate]') - .orderBy('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') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .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) => { - let 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 parseInt(row.count, 10); - }); - } -}; - -module.exports = internalProxyHost; diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js deleted file mode 100644 index f22c3668..00000000 --- a/backend/internal/redirection-host.js +++ /dev/null @@ -1,461 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const redirectionHostModel = require('../models/redirection_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalRedirectionHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('redirection_hosts:create', data) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return redirectionHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalRedirectionHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - 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) => { - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'redirection-host', - 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 create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('redirection_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalRedirectionHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return redirectionHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'redirection-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalRedirectionHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(redirectionHostModel, 'redirection_host', row) - .then((new_meta) => { - row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(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) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('redirection_hosts:get', data.id) - .then((access_data) => { - let query = redirectionHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @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) { - throw new error.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) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.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) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.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) => { - let query = redirectionHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,certificate]') - .orderBy('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') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .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) => { - let 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 parseInt(row.count, 10); - }); - } -}; - -module.exports = internalRedirectionHost; diff --git a/backend/internal/report.js b/backend/internal/report.js deleted file mode 100644 index 4dde659b..00000000 --- a/backend/internal/report.js +++ /dev/null @@ -1,38 +0,0 @@ -const internalProxyHost = require('./proxy-host'); -const internalRedirectionHost = require('./redirection-host'); -const internalDeadHost = require('./dead-host'); -const internalStream = require('./stream'); - -const internalReport = { - - /** - * @param {Access} access - * @return {Promise} - */ - getHostsReport: (access) => { - return access.can('reports:hosts', 1) - .then((access_data) => { - let user_id = access.token.getUserId(1); - - let promises = [ - internalProxyHost.getCount(user_id, access_data.visibility), - internalRedirectionHost.getCount(user_id, access_data.visibility), - internalStream.getCount(user_id, access_data.visibility), - internalDeadHost.getCount(user_id, access_data.visibility) - ]; - - return Promise.all(promises); - }) - .then((counts) => { - return { - proxy: counts.shift(), - redirection: counts.shift(), - stream: counts.shift(), - dead: counts.shift() - }; - }); - - } -}; - -module.exports = internalReport; diff --git a/backend/internal/setting.js b/backend/internal/setting.js deleted file mode 100644 index d4ac67d8..00000000 --- a/backend/internal/setting.js +++ /dev/null @@ -1,133 +0,0 @@ -const fs = require('fs'); -const error = require('../lib/error'); -const settingModel = require('../models/setting'); -const internalNginx = require('./nginx'); - -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 error.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 error.ValidationError('Could not reconfigure Nginx. Please check logs.'); - }); - }); - } else { - 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; - } else { - throw new error.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 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'); - }); - } -}; - -module.exports = internalSetting; diff --git a/backend/internal/state/state.go b/backend/internal/state/state.go new file mode 100644 index 00000000..0fa1433f --- /dev/null +++ b/backend/internal/state/state.go @@ -0,0 +1,31 @@ +package state + +import ( + "sync" +) + +// AppState holds pointers to channels and waitGroups +// shared by all goroutines of the application +type AppState struct { + waitGroup sync.WaitGroup + termSig chan bool +} + +// NewState creates a new app state +func NewState() *AppState { + state := &AppState{ + // buffered channel + termSig: make(chan bool, 1), + } + return state +} + +// GetWaitGroup returns the state's wg +func (state *AppState) GetWaitGroup() *sync.WaitGroup { + return &state.waitGroup +} + +// GetTermSig returns the state's term signal +func (state *AppState) GetTermSig() chan bool { + return state.termSig +} diff --git a/backend/internal/stream.js b/backend/internal/stream.js deleted file mode 100644 index 9c458a10..00000000 --- a/backend/internal/stream.js +++ /dev/null @@ -1,348 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const streamModel = require('../models/stream'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); - -function omissions () { - return ['is_deleted']; -} - -const internalStream = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - 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 = {}; - } - - return streamModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(streamModel, 'stream', row) - .then(() => { - return internalStream.get(access, {id: row.id, expand: ['owner']}); - }); - }) - .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) => { - return access.can('streams:update', data.id) - .then((/*access_data*/) => { - // TODO: at this point the existing streams should have been checked - return internalStream.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - return streamModel - .query() - .omit(omissions()) - .patchAndFetchById(row.id, data) - .then((saved_row) => { - return internalNginx.configure(streamModel, 'stream', saved_row) - .then(() => { - return internalStream.get(access, {id: row.id, expand: ['owner']}); - }); - }) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'stream', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_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) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('streams:get', data.id) - .then((access_data) => { - let query = streamModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @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) { - throw new error.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: ['owner'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host 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) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host 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-host', - 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) => { - let query = streamModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner]') - .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') { - query.where(function () { - this.where('incoming_port', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let 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 parseInt(row.count, 10); - }); - } -}; - -module.exports = internalStream; diff --git a/backend/internal/token.js b/backend/internal/token.js deleted file mode 100644 index a64b9010..00000000 --- a/backend/internal/token.js +++ /dev/null @@ -1,162 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const authModel = require('../models/auth'); -const helpers = require('../lib/helpers'); -const TokenModel = require('../models/token'); - -module.exports = { - - /** - * @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: (data, issuer) => { - let Token = new TokenModel(); - - data.scope = data.scope || 'user'; - data.expiry = data.expiry || '1d'; - - return userModel - .query() - .where('email', data.identity) - .andWhere('is_deleted', 0) - .andWhere('is_disabled', 0) - .first() - .then((user) => { - if (user) { - // Get auth - return authModel - .query() - .where('user_id', '=', user.id) - .where('type', '=', 'password') - .first() - .then((auth) => { - if (auth) { - return auth.verifyPassword(data.secret) - .then((valid) => { - if (valid) { - - 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 error.AuthError('Invalid scope: ' + data.scope); - } - - // Create a moment of the expiry expression - let expiry = helpers.parseDatePeriod(data.expiry); - if (expiry === null) { - throw new error.AuthError('Invalid expiry time: ' + data.expiry); - } - - return Token.create({ - iss: issuer || 'api', - attrs: { - id: user.id - }, - scope: [data.scope], - expiresIn: data.expiry - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString() - }; - }); - } else { - throw new error.AuthError('Invalid password'); - } - }); - } else { - throw new error.AuthError('No password auth for user'); - } - }); - } else { - throw new error.AuthError('No relevant user found'); - } - }); - }, - - /** - * @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: (access, data) => { - let Token = new TokenModel(); - - data = data || {}; - data.expiry = data.expiry || '1d'; - - if (access && access.token.getUserId(0)) { - - // Create a moment of the expiry expression - let expiry = helpers.parseDatePeriod(data.expiry); - if (expiry === null) { - throw new error.AuthError('Invalid expiry time: ' + data.expiry); - } - - let token_attrs = { - id: access.token.getUserId(0) - }; - - // Only admins can request otherwise scoped tokens - let scope = access.token.get('scope'); - if (data.scope && access.token.hasScope('admin')) { - scope = [data.scope]; - - if (data.scope === 'job-board' || data.scope === 'worker') { - token_attrs.id = 0; - } - } - - return Token.create({ - iss: 'api', - scope: scope, - attrs: token_attrs, - expiresIn: data.expiry - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString() - }; - }); - } else { - throw new error.AssertionFailedError('Existing token contained invalid user data'); - } - }, - - /** - * @param {Object} user - * @returns {Promise} - */ - getTokenFromUser: (user) => { - const expire = '1d'; - const Token = new TokenModel(); - const expiry = helpers.parseDatePeriod(expire); - - return Token.create({ - iss: 'api', - attrs: { - id: user.id - }, - scope: ['user'], - expiresIn: expire - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString(), - user: user - }; - }); - } -}; diff --git a/backend/internal/types/db_date.go b/backend/internal/types/db_date.go new file mode 100644 index 00000000..9339868e --- /dev/null +++ b/backend/internal/types/db_date.go @@ -0,0 +1,39 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +// DBDate is a date time +// type DBDate time.Time +type DBDate struct { + Time time.Time +} + +// Value encodes the type ready for the database +func (d DBDate) Value() (driver.Value, error) { + return driver.Value(d.Time.Unix()), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *DBDate) Scan(src interface{}) error { + d.Time = time.Unix(src.(int64), 0) + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *DBDate) UnmarshalJSON(data []byte) error { + var u int64 + if err := json.Unmarshal(data, &u); err != nil { + return err + } + d.Time = time.Unix(u, 0) + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d DBDate) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Time.Unix()) +} diff --git a/backend/internal/types/jsonb.go b/backend/internal/types/jsonb.go new file mode 100644 index 00000000..ff3f7930 --- /dev/null +++ b/backend/internal/types/jsonb.go @@ -0,0 +1,71 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// JSONB can be anything +type JSONB struct { + Encoded string `json:"decoded"` + Decoded interface{} `json:"encoded"` +} + +// Value encodes the type ready for the database +func (j JSONB) Value() (driver.Value, error) { + json, err := json.Marshal(j.Decoded) + return driver.Value(string(json)), err +} + +// Scan takes data from the database and modifies it for Go Types +func (j *JSONB) Scan(src interface{}) error { + var jsonb JSONB + var srcString string + switch v := src.(type) { + case string: + srcString = src.(string) + case []uint8: + srcString = string(src.([]uint8)) + default: + return fmt.Errorf("Incompatible type for JSONB: %v", v) + } + + jsonb.Encoded = srcString + + if err := json.Unmarshal([]byte(srcString), &jsonb.Decoded); err != nil { + return err + } + + *j = jsonb + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (j *JSONB) UnmarshalJSON(data []byte) error { + var jsonb JSONB + jsonb.Encoded = string(data) + if err := json.Unmarshal(data, &jsonb.Decoded); err != nil { + return err + } + *j = jsonb + return nil +} + +// MarshalJSON will marshal for output in api responses +func (j JSONB) MarshalJSON() ([]byte, error) { + return json.Marshal(j.Decoded) +} + +// AsStringArray will attempt to return as []string +func (j JSONB) AsStringArray() ([]string, error) { + var strs []string + + // Encode then Decode onto this type + b, _ := j.MarshalJSON() + if err := json.Unmarshal(b, &strs); err != nil { + return strs, err + } + + return strs, nil +} diff --git a/backend/internal/types/nullable_db_date.go b/backend/internal/types/nullable_db_date.go new file mode 100644 index 00000000..9a0022c2 --- /dev/null +++ b/backend/internal/types/nullable_db_date.go @@ -0,0 +1,54 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +// NullableDBDate is a date time that can be null in the db +// type DBDate time.Time +type NullableDBDate struct { + Time *time.Time +} + +// Value encodes the type ready for the database +func (d NullableDBDate) Value() (driver.Value, error) { + if d.Time == nil { + return nil, nil + } + return driver.Value(d.Time.Unix()), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBDate) Scan(src interface{}) error { + var tme time.Time + if src != nil { + tme = time.Unix(src.(int64), 0) + } + + d.Time = &tme + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBDate) UnmarshalJSON(data []byte) error { + var t time.Time + var u int64 + if err := json.Unmarshal(data, &u); err != nil { + d.Time = &t + return nil + } + t = time.Unix(u, 0) + d.Time = &t + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBDate) MarshalJSON() ([]byte, error) { + if d.Time == nil || d.Time.IsZero() { + return json.Marshal(nil) + } + + return json.Marshal(d.Time.Unix()) +} diff --git a/backend/internal/user.js b/backend/internal/user.js deleted file mode 100644 index 2e2d8abf..00000000 --- a/backend/internal/user.js +++ /dev/null @@ -1,518 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const userPermissionModel = require('../models/user_permission'); -const authModel = require('../models/auth'); -const gravatar = require('gravatar'); -const internalToken = require('./token'); -const internalAuditLog = require('./audit-log'); - -function omissions () { - return ['is_deleted']; -} - -const internalUser = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let 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; - } - - return access.can('users:create', data) - .then(() => { - data.avatar = gravatar.url(data.email, {default: 'mm'}); - - return userModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((user) => { - if (auth) { - return authModel - .query() - .insert({ - user_id: user.id, - type: auth.type, - secret: auth.secret, - meta: {} - }) - .then(() => { - return user; - }); - } else { - return user; - } - }) - .then((user) => { - // Create permissions row as well - let is_admin = data.roles.indexOf('admin') !== -1; - - return userPermissionModel - .query() - .insert({ - user_id: user.id, - visibility: is_admin ? 'all' : 'user', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage' - }) - .then(() => { - return internalUser.get(access, {id: user.id, expand: ['permissions']}); - }); - }) - .then((user) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'user', - object_id: user.id, - meta: user - }) - .then(() => { - 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 error.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 error.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() - .omit(omissions()) - .patchAndFetchById(user.id, data) - .then((saved_user) => { - return _.omit(saved_user, 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 - }) - .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) => { - if (typeof data === 'undefined') { - data = {}; - } - - if (typeof data.id === 'undefined' || !data.id) { - data.id = access.token.getUserId(0); - } - - return access.can('users:get', data.id) - .then(() => { - let query = userModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[permissions]') - .first(); - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * 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) => { - let 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 error.ItemNotFoundError(data.id); - } - - // Make sure user can't delete themselves - if (user.id === access.token.getUserId(0)) { - throw new error.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; - }); - }, - - /** - * This will only count the users - * - * @param {Access} access - * @param {String} [search_query] - * @returns {*} - */ - getCount: (access, search_query) => { - return access.can('users:list') - .then(() => { - let 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 parseInt(row.count, 10); - }); - }, - - /** - * All users - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('users:list') - .then(() => { - let query = userModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[permissions]') - .orderBy('name', 'ASC'); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%') - .orWhere('email', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * @param {Access} access - * @param {Integer} [id_requested] - * @returns {[String]} - */ - getUserOmisionsByAccess: (access, id_requested) => { - let response = []; // Admin response - - if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) { - response = ['roles', 'is_deleted']; // Restricted response - } - - return response; - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} data.type - * @param {String} data.secret - * @return {Promise} - */ - setPassword: (access, data) => { - return access.can('users:password', data.id) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); - } - - if (user.id === access.token.getUserId(0)) { - // they're setting their own password. Make sure their current password is correct - if (typeof data.current === 'undefined' || !data.current) { - throw new error.ValidationError('Current password was not supplied'); - } - - return internalToken.getTokenFromEmail({ - identity: user.email, - secret: data.current - }) - .then(() => { - return user; - }); - } - - return user; - }) - .then((user) => { - // Get auth, patch if it exists - return authModel - .query() - .where('user_id', user.id) - .andWhere('type', data.type) - .first() - .then((existing_auth) => { - if (existing_auth) { - // patch - return authModel - .query() - .where('user_id', user.id) - .andWhere('type', data.type) - .patch({ - type: data.type, // This is required for the model to encrypt on save - secret: data.secret - }); - } else { - // insert - return authModel - .query() - .insert({ - user_id: user.id, - type: data.type, - secret: data.secret, - meta: {} - }); - } - }) - .then(() => { - // Add to Audit Log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: { - name: user.name, - password_changed: true, - auth_type: data.type - } - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @return {Promise} - */ - setPermissions: (access, data) => { - return access.can('users:permissions', data.id) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); - } - - return user; - }) - .then((user) => { - // Get perms row, patch if it exists - return userPermissionModel - .query() - .where('user_id', user.id) - .first() - .then((existing_auth) => { - if (existing_auth) { - // patch - return userPermissionModel - .query() - .where('user_id', user.id) - .patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data)); - } else { - // insert - return userPermissionModel - .query() - .insertAndFetch(_.assign({user_id: user.id}, data)); - } - }) - .then((permissions) => { - // Add to Audit Log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: { - name: user.name, - permissions: permissions - } - }); - - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - */ - loginAs: (access, data) => { - return access.can('users:loginas', data.id) - .then(() => { - return internalUser.get(access, data); - }) - .then((user) => { - return internalToken.getTokenFromUser(user); - }); - } -}; - -module.exports = internalUser; diff --git a/backend/internal/util/interfaces.go b/backend/internal/util/interfaces.go new file mode 100644 index 00000000..ebb6ffa3 --- /dev/null +++ b/backend/internal/util/interfaces.go @@ -0,0 +1,36 @@ +package util + +// FindItemInInterface Find key in interface (recursively) and return value as interface +func FindItemInInterface(key string, obj interface{}) (interface{}, bool) { + // if the argument is not a map, ignore it + mobj, ok := obj.(map[string]interface{}) + if !ok { + return nil, false + } + + for k, v := range mobj { + // key match, return value + if k == key { + return v, true + } + + // if the value is a map, search recursively + if m, ok := v.(map[string]interface{}); ok { + if res, ok := FindItemInInterface(key, m); ok { + return res, true + } + } + // if the value is an array, search recursively + // from each element + if va, ok := v.([]interface{}); ok { + for _, a := range va { + if res, ok := FindItemInInterface(key, a); ok { + return res, true + } + } + } + } + + // element not found + return nil, false +} diff --git a/backend/internal/util/maps.go b/backend/internal/util/maps.go new file mode 100644 index 00000000..1ff211ec --- /dev/null +++ b/backend/internal/util/maps.go @@ -0,0 +1,9 @@ +package util + +// MapContainsKey is fairly self explanatory +func MapContainsKey(dict map[string]interface{}, key string) bool { + if _, ok := dict[key]; ok { + return true + } + return false +} diff --git a/backend/internal/util/maps_test.go b/backend/internal/util/maps_test.go new file mode 100644 index 00000000..fdb5d2ff --- /dev/null +++ b/backend/internal/util/maps_test.go @@ -0,0 +1,45 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type rect struct { + width int + height int +} + +func TestMapContainsKey(t *testing.T) { + var r rect + r.width = 5 + r.height = 5 + m := map[string]interface{}{ + "rect_width": r.width, + "rect_height": r.height, + } + tests := []struct { + name string + pass string + want bool + }{ + { + name: "exists", + pass: "rect_width", + want: true, + }, + { + name: "Does not exist", + pass: "rect_perimeter", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MapContainsKey(m, tt.pass) + + assert.Equal(t, result, tt.want) + }) + } +} diff --git a/backend/internal/util/slices.go b/backend/internal/util/slices.go new file mode 100644 index 00000000..9dfdcb8d --- /dev/null +++ b/backend/internal/util/slices.go @@ -0,0 +1,44 @@ +package util + +import ( + "strconv" + "strings" +) + +// SliceContainsItem returns whether the slice given contains the item given +func SliceContainsItem(slice []string, item string) bool { + for _, a := range slice { + if a == item { + return true + } + } + return false +} + +// SliceContainsInt returns whether the slice given contains the item given +func SliceContainsInt(slice []int, item int) bool { + for _, a := range slice { + if a == item { + return true + } + } + return false +} + +// ConvertIntSliceToString returns a comma separated string of all items in the slice +func ConvertIntSliceToString(slice []int) string { + strs := []string{} + for _, item := range slice { + strs = append(strs, strconv.Itoa(item)) + } + return strings.Join(strs, ",") +} + +// ConvertStringSliceToInterface is required in some special cases +func ConvertStringSliceToInterface(slice []string) []interface{} { + res := make([]interface{}, len(slice)) + for i := range slice { + res[i] = slice[i] + } + return res +} diff --git a/backend/internal/util/slices_test.go b/backend/internal/util/slices_test.go new file mode 100644 index 00000000..f2f18714 --- /dev/null +++ b/backend/internal/util/slices_test.go @@ -0,0 +1,92 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSliceContainsItem(t *testing.T) { + type want struct { + result bool + } + tests := []struct { + name string + inputString string + inputArray []string + want want + }{ + { + name: "In array", + inputString: "test", + inputArray: []string{"no", "more", "tests", "test"}, + want: want{ + result: true, + }, + }, + { + name: "Not in array", + inputString: "test", + inputArray: []string{"no", "more", "tests"}, + want: want{ + result: false, + }, + }, + { + name: "Case sensitive", + inputString: "test", + inputArray: []string{"no", "TEST", "more"}, + want: want{ + result: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SliceContainsItem(tt.inputArray, tt.inputString) + assert.Equal(t, tt.want.result, got) + }) + } +} + +func TestSliceContainsInt(t *testing.T) { + type want struct { + result bool + } + tests := []struct { + name string + inputInt int + inputArray []int + want want + }{ + { + name: "In array", + inputInt: 1, + inputArray: []int{1, 2, 3, 4}, + want: want{ + result: true, + }, + }, + { + name: "Not in array", + inputInt: 1, + inputArray: []int{10, 2, 3, 4}, + want: want{ + result: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SliceContainsInt(tt.inputArray, tt.inputInt) + assert.Equal(t, tt.want.result, got) + }) + } +} + +func TestConvertIntSliceToString(t *testing.T) { + items := []int{1, 2, 3, 4, 5, 6, 7} + expectedStr := "1,2,3,4,5,6,7" + str := ConvertIntSliceToString(items) + assert.Equal(t, expectedStr, str) +} diff --git a/backend/internal/validator/hosts.go b/backend/internal/validator/hosts.go new file mode 100644 index 00000000..f830d2de --- /dev/null +++ b/backend/internal/validator/hosts.go @@ -0,0 +1,33 @@ +package validator + +import ( + "fmt" + + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/hosttemplate" +) + +// ValidateHost will check if associated objects exist and other checks +// will return a nil error if things are OK +func ValidateHost(h host.Model) error { + if h.CertificateID > 0 { + // Check certificate exists and is valid + // This will not determine if the certificate is Ready to use, + // as this validation only cares that the row exists. + if _, cErr := certificate.GetByID(h.CertificateID); cErr != nil { + return fmt.Errorf("Certificate #%d does not exist", h.CertificateID) + } + } + + // Check the host template exists and has the same type. + hostTemplate, tErr := hosttemplate.GetByID(h.HostTemplateID) + if tErr != nil { + return fmt.Errorf("Host Template #%d does not exist", h.HostTemplateID) + } + if hostTemplate.Type != h.Type { + return fmt.Errorf("Host Template #%d is not valid for this host type", h.HostTemplateID) + } + + return nil +} diff --git a/backend/internal/worker/certificate.go b/backend/internal/worker/certificate.go new file mode 100644 index 00000000..a108010f --- /dev/null +++ b/backend/internal/worker/certificate.go @@ -0,0 +1,63 @@ +package worker + +import ( + "time" + + "npm/internal/entity/certificate" + "npm/internal/logger" + "npm/internal/state" +) + +type certificateWorker struct { + state *state.AppState +} + +// StartCertificateWorker starts the CertificateWorker +func StartCertificateWorker(state *state.AppState) { + worker := newCertificateWorker(state) + logger.Info("CertificateWorker Started") + worker.Run() +} + +func newCertificateWorker(state *state.AppState) *certificateWorker { + return &certificateWorker{ + state: state, + } +} + +// Run the CertificateWorker +func (w *certificateWorker) Run() { + // global wait group + gwg := w.state.GetWaitGroup() + gwg.Add(1) + + ticker := time.NewTicker(15 * time.Second) +mainLoop: + for { + select { + case _, more := <-w.state.GetTermSig(): + if !more { + logger.Info("Terminating CertificateWorker ... ") + break mainLoop + } + case <-ticker.C: + // Can confirm that this will wait for completion before the next loop + requestCertificates() + } + } +} + +func requestCertificates() { + // logger.Debug("requestCertificates fired") + rows, err := certificate.GetByStatus(certificate.StatusReady) + if err != nil { + logger.Error("requestCertificatesError", err) + return + } + + for _, row := range rows { + if err := row.Request(); err != nil { + logger.Error("CertificateRequestError", err) + } + } +} diff --git a/backend/knexfile.js b/backend/knexfile.js deleted file mode 100644 index 391ca005..00000000 --- a/backend/knexfile.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - development: { - client: 'mysql', - migrations: { - tableName: 'migrations', - stub: 'lib/migrate_template.js', - directory: 'migrations' - } - }, - - production: { - client: 'mysql', - migrations: { - tableName: 'migrations', - stub: 'lib/migrate_template.js', - directory: 'migrations' - } - } -}; diff --git a/backend/lib/access.js b/backend/lib/access.js deleted file mode 100644 index 9d7329d9..00000000 --- a/backend/lib/access.js +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Some Notes: This is a friggin complicated piece of code. - * - * "scope" in this file means "where did this token come from and what is using it", so 99% of the time - * the "scope" is going to be "user" because it would be a user token. This is not to be confused with - * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. - * - * - */ - -const _ = require('lodash'); -const logger = require('../logger').access; -const validator = require('ajv'); -const error = require('./error'); -const userModel = require('../models/user'); -const proxyHostModel = require('../models/proxy_host'); -const TokenModel = require('../models/token'); -const roleSchema = require('./access/roles.json'); -const permsSchema = require('./access/permissions.json'); - -module.exports = function (token_string) { - let Token = new TokenModel(); - let token_data = null; - let initialised = false; - let object_cache = {}; - let allow_internal_access = false; - let user_roles = []; - let permissions = {}; - - /** - * Loads the Token object from the token string - * - * @returns {Promise} - */ - this.init = () => { - return new Promise((resolve, reject) => { - if (initialised) { - resolve(); - } else if (!token_string) { - reject(new error.PermissionError('Permission Denied')); - } else { - resolve(Token.load(token_string) - .then((data) => { - token_data = data; - - // At this point we need to load the user from the DB and make sure they: - // - exist (and not soft deleted) - // - still have the appropriate scopes for this token - // This is only required when the User ID is supplied or if the token scope has `user` - - if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) { - // Has token user id or token user scope - return userModel - .query() - .where('id', token_data.attrs.id) - .andWhere('is_deleted', 0) - .andWhere('is_disabled', 0) - .allowEager('[permissions]') - .eager('[permissions]') - .first() - .then((user) => { - if (user) { - // make sure user has all scopes of the token - // The `user` role is not added against the user row, so we have to just add it here to get past this check. - user.roles.push('user'); - - let is_ok = true; - _.forEach(token_data.scope, (scope_item) => { - if (_.indexOf(user.roles, scope_item) === -1) { - is_ok = false; - } - }); - - if (!is_ok) { - throw new error.AuthError('Invalid token scope for User'); - } else { - initialised = true; - user_roles = user.roles; - permissions = user.permissions; - } - - } else { - throw new error.AuthError('User cannot be loaded for Token'); - } - }); - } else { - initialised = true; - } - })); - } - }); - }; - - /** - * Fetches the object ids from the database, only once per object type, for this token. - * This only applies to USER token scopes, as all other tokens are not really bound - * by object scopes - * - * @param {String} object_type - * @returns {Promise} - */ - this.loadObjects = (object_type) => { - return new Promise((resolve, reject) => { - if (Token.hasScope('user')) { - if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) { - reject(new error.AuthError('User Token supplied without a User ID')); - } else { - let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0; - let query; - - if (typeof object_cache[object_type] === 'undefined') { - switch (object_type) { - - // USERS - should only return yourself - case 'users': - resolve(token_user_id ? [token_user_id] : []); - break; - - // Proxy Hosts - case 'proxy_hosts': - query = proxyHostModel - .query() - .select('id') - .andWhere('is_deleted', 0); - - if (permissions.visibility === 'user') { - query.andWhere('owner_user_id', token_user_id); - } - - resolve(query - .then((rows) => { - let result = []; - _.forEach(rows, (rule_row) => { - result.push(rule_row.id); - }); - - // enum should not have less than 1 item - if (!result.length) { - result.push(0); - } - - return result; - }) - ); - break; - - // DEFAULT: null - default: - resolve(null); - break; - } - } else { - resolve(object_cache[object_type]); - } - } - } else { - resolve(null); - } - }) - .then((objects) => { - object_cache[object_type] = objects; - return objects; - }); - }; - - /** - * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema - * - * @param {String} permission_label - * @returns {Object} - */ - this.getObjectSchema = (permission_label) => { - let base_object_type = permission_label.split(':').shift(); - - let schema = { - $id: 'objects', - $schema: 'http://json-schema.org/draft-07/schema#', - description: 'Actor Properties', - type: 'object', - additionalProperties: false, - properties: { - user_id: { - anyOf: [ - { - type: 'number', - enum: [Token.get('attrs').id] - } - ] - }, - scope: { - type: 'string', - pattern: '^' + Token.get('scope') + '$' - } - } - }; - - return this.loadObjects(base_object_type) - .then((object_result) => { - if (typeof object_result === 'object' && object_result !== null) { - schema.properties[base_object_type] = { - type: 'number', - enum: object_result, - minimum: 1 - }; - } else { - schema.properties[base_object_type] = { - type: 'number', - minimum: 1 - }; - } - - return schema; - }); - }; - - return { - - token: Token, - - /** - * - * @param {Boolean} [allow_internal] - * @returns {Promise} - */ - load: (allow_internal) => { - return new Promise(function (resolve/*, reject*/) { - if (token_string) { - resolve(Token.load(token_string)); - } else { - allow_internal_access = allow_internal; - resolve(allow_internal_access || null); - } - }); - }, - - reloadObjects: this.loadObjects, - - /** - * - * @param {String} permission - * @param {*} [data] - * @returns {Promise} - */ - can: (permission, data) => { - if (allow_internal_access === true) { - return Promise.resolve(true); - //return true; - } else { - return this.init() - .then(() => { - // Initialised, token decoded ok - return this.getObjectSchema(permission) - .then((objectSchema) => { - let data_schema = { - [permission]: { - data: data, - scope: Token.get('scope'), - roles: user_roles, - permission_visibility: permissions.visibility, - permission_proxy_hosts: permissions.proxy_hosts, - permission_redirection_hosts: permissions.redirection_hosts, - permission_dead_hosts: permissions.dead_hosts, - permission_streams: permissions.streams, - permission_access_lists: permissions.access_lists, - permission_certificates: permissions.certificates - } - }; - - let permissionSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $async: true, - $id: 'permissions', - additionalProperties: false, - properties: {} - }; - - permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json'); - - // logger.info('objectSchema', JSON.stringify(objectSchema, null, 2)); - // logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2)); - // logger.info('data_schema', JSON.stringify(data_schema, null, 2)); - - let ajv = validator({ - verbose: true, - allErrors: true, - format: 'full', - missingRefs: 'fail', - breakOnError: true, - coerceTypes: true, - schemas: [ - roleSchema, - permsSchema, - objectSchema, - permissionSchema - ] - }); - - return ajv.validate('permissions', data_schema) - .then(() => { - return data_schema[permission]; - }); - }); - }) - .catch((err) => { - err.permission = permission; - err.permission_data = data; - logger.error(permission, data, err.message); - - throw new error.PermissionError('Permission Denied', err); - }); - } - } - }; -}; diff --git a/backend/lib/access/access_lists-create.json b/backend/lib/access/access_lists-create.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-delete.json b/backend/lib/access/access_lists-delete.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-get.json b/backend/lib/access/access_lists-get.json deleted file mode 100644 index 8f6dd8cc..00000000 --- a/backend/lib/access/access_lists-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-list.json b/backend/lib/access/access_lists-list.json deleted file mode 100644 index 8f6dd8cc..00000000 --- a/backend/lib/access/access_lists-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-update.json b/backend/lib/access/access_lists-update.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/auditlog-list.json b/backend/lib/access/auditlog-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/auditlog-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/certificates-create.json b/backend/lib/access/certificates-create.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-delete.json b/backend/lib/access/certificates-delete.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-get.json b/backend/lib/access/certificates-get.json deleted file mode 100644 index 9ccfa4f1..00000000 --- a/backend/lib/access/certificates-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-list.json b/backend/lib/access/certificates-list.json deleted file mode 100644 index 9ccfa4f1..00000000 --- a/backend/lib/access/certificates-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-update.json b/backend/lib/access/certificates-update.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-create.json b/backend/lib/access/dead_hosts-create.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-delete.json b/backend/lib/access/dead_hosts-delete.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-get.json b/backend/lib/access/dead_hosts-get.json deleted file mode 100644 index 87aa12e7..00000000 --- a/backend/lib/access/dead_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-list.json b/backend/lib/access/dead_hosts-list.json deleted file mode 100644 index 87aa12e7..00000000 --- a/backend/lib/access/dead_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-update.json b/backend/lib/access/dead_hosts-update.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/permissions.json b/backend/lib/access/permissions.json deleted file mode 100644 index 8480f9a1..00000000 --- a/backend/lib/access/permissions.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "perms", - "definitions": { - "view": { - "type": "string", - "pattern": "^(view|manage)$" - }, - "manage": { - "type": "string", - "pattern": "^(manage)$" - } - } -} diff --git a/backend/lib/access/proxy_hosts-create.json b/backend/lib/access/proxy_hosts-create.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-delete.json b/backend/lib/access/proxy_hosts-delete.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-get.json b/backend/lib/access/proxy_hosts-get.json deleted file mode 100644 index d88e4cff..00000000 --- a/backend/lib/access/proxy_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-list.json b/backend/lib/access/proxy_hosts-list.json deleted file mode 100644 index d88e4cff..00000000 --- a/backend/lib/access/proxy_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-update.json b/backend/lib/access/proxy_hosts-update.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-create.json b/backend/lib/access/redirection_hosts-create.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-delete.json b/backend/lib/access/redirection_hosts-delete.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-get.json b/backend/lib/access/redirection_hosts-get.json deleted file mode 100644 index ba229206..00000000 --- a/backend/lib/access/redirection_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-list.json b/backend/lib/access/redirection_hosts-list.json deleted file mode 100644 index ba229206..00000000 --- a/backend/lib/access/redirection_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-update.json b/backend/lib/access/redirection_hosts-update.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/reports-hosts.json b/backend/lib/access/reports-hosts.json deleted file mode 100644 index dbc9e0c0..00000000 --- a/backend/lib/access/reports-hosts.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/user" - } - ] -} diff --git a/backend/lib/access/roles.json b/backend/lib/access/roles.json deleted file mode 100644 index 16b33b55..00000000 --- a/backend/lib/access/roles.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "roles", - "definitions": { - "admin": { - "type": "object", - "required": ["scope", "roles"], - "properties": { - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - }, - "roles": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^admin$" - } - } - } - }, - "user": { - "type": "object", - "required": ["scope"], - "properties": { - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - } -} diff --git a/backend/lib/access/settings-get.json b/backend/lib/access/settings-get.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-get.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/settings-list.json b/backend/lib/access/settings-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/settings-update.json b/backend/lib/access/settings-update.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-update.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/streams-create.json b/backend/lib/access/streams-create.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-delete.json b/backend/lib/access/streams-delete.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-get.json b/backend/lib/access/streams-get.json deleted file mode 100644 index 7e996287..00000000 --- a/backend/lib/access/streams-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-list.json b/backend/lib/access/streams-list.json deleted file mode 100644 index 7e996287..00000000 --- a/backend/lib/access/streams-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-update.json b/backend/lib/access/streams-update.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/users-create.json b/backend/lib/access/users-create.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-create.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-delete.json b/backend/lib/access/users-delete.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-delete.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-get.json b/backend/lib/access/users-get.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/access/users-list.json b/backend/lib/access/users-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-loginas.json b/backend/lib/access/users-loginas.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-loginas.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-password.json b/backend/lib/access/users-password.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-password.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/access/users-permissions.json b/backend/lib/access/users-permissions.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-permissions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-update.json b/backend/lib/access/users-update.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/error.js b/backend/lib/error.js deleted file mode 100644 index 9e456f05..00000000 --- a/backend/lib/error.js +++ /dev/null @@ -1,90 +0,0 @@ -const _ = require('lodash'); -const util = require('util'); - -module.exports = { - - PermissionError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = 'Permission Denied'; - this.public = true; - this.status = 403; - }, - - ItemNotFoundError: function (id, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = 'Item Not Found - ' + id; - this.public = true; - this.status = 404; - }, - - AuthError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = true; - this.status = 401; - }, - - InternalError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 500; - this.public = false; - }, - - InternalValidationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 400; - this.public = false; - }, - - ConfigurationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 400; - this.public = true; - }, - - CacheError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; - this.previous = previous; - this.status = 500; - this.public = false; - }, - - ValidationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = true; - this.status = 400; - }, - - AssertionFailedError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = false; - this.status = 400; - } -}; - -_.forEach(module.exports, function (error) { - util.inherits(error, Error); -}); diff --git a/backend/lib/express/cors.js b/backend/lib/express/cors.js deleted file mode 100644 index c9befeec..00000000 --- a/backend/lib/express/cors.js +++ /dev/null @@ -1,40 +0,0 @@ -const validator = require('../validator'); - -module.exports = function (req, res, next) { - - if (req.headers.origin) { - - const originSchema = { - oneOf: [ - { - type: 'string', - pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$' - }, - { - type: 'string', - pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$' - } - ] - }; - - // very relaxed validation.... - validator(originSchema, req.headers.origin) - .then(function () { - res.set({ - 'Access-Control-Allow-Origin': req.headers.origin, - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', - 'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit', - 'Access-Control-Max-Age': 5 * 60, - 'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit' - }); - next(); - }) - .catch(next); - - } else { - // No origin - next(); - } - -}; diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js deleted file mode 100644 index 17edccec..00000000 --- a/backend/lib/express/jwt-decode.js +++ /dev/null @@ -1,15 +0,0 @@ -const Access = require('../access'); - -module.exports = () => { - return function (req, res, next) { - res.locals.access = null; - let access = new Access(res.locals.token || null); - access.load() - .then(() => { - res.locals.access = access; - next(); - }) - .catch(next); - }; -}; - diff --git a/backend/lib/express/jwt.js b/backend/lib/express/jwt.js deleted file mode 100644 index 44aa3693..00000000 --- a/backend/lib/express/jwt.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = function () { - return function (req, res, next) { - if (req.headers.authorization) { - let parts = req.headers.authorization.split(' '); - - if (parts && parts[0] === 'Bearer' && parts[1]) { - res.locals.token = parts[1]; - } - } - - next(); - }; -}; diff --git a/backend/lib/express/pagination.js b/backend/lib/express/pagination.js deleted file mode 100644 index 24ffa58d..00000000 --- a/backend/lib/express/pagination.js +++ /dev/null @@ -1,55 +0,0 @@ -let _ = require('lodash'); - -module.exports = function (default_sort, default_offset, default_limit, max_limit) { - - /** - * This will setup the req query params with filtered data and defaults - * - * sort will be an array of fields and their direction - * offset will be an int, defaulting to zero if no other default supplied - * limit will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied - * - */ - - return function (req, res, next) { - - req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10); - req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10); - - if (max_limit && req.query.limit > max_limit) { - req.query.limit = max_limit; - } - - // Sorting - let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort; - let myRegexp = /.*\.(asc|desc)$/ig; - let sort_array = []; - - sort = sort.split(','); - _.map(sort, function (val) { - let matches = myRegexp.exec(val); - - if (matches !== null) { - let dir = matches[1]; - sort_array.push({ - field: val.substr(0, val.length - (dir.length + 1)), - dir: dir.toLowerCase() - }); - } else { - sort_array.push({ - field: val, - dir: 'asc' - }); - } - }); - - // Sort will now be in this format: - // [ - // { field: 'field1', dir: 'asc' }, - // { field: 'field2', dir: 'desc' } - // ] - - req.query.sort = sort_array; - next(); - }; -}; diff --git a/backend/lib/express/user-id-from-me.js b/backend/lib/express/user-id-from-me.js deleted file mode 100644 index 4a37a406..00000000 --- a/backend/lib/express/user-id-from-me.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = (req, res, next) => { - if (req.params.user_id === 'me' && res.locals.access) { - req.params.user_id = res.locals.access.token.get('attrs').id; - } else { - req.params.user_id = parseInt(req.params.user_id, 10); - } - - next(); -}; diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js deleted file mode 100644 index e38be991..00000000 --- a/backend/lib/helpers.js +++ /dev/null @@ -1,32 +0,0 @@ -const moment = require('moment'); - -module.exports = { - - /** - * Takes an expression such as 30d and returns a moment object of that date in future - * - * Key Shorthand - * ================== - * years y - * quarters Q - * months M - * weeks w - * days d - * hours h - * minutes m - * seconds s - * milliseconds ms - * - * @param {String} expression - * @returns {Object} - */ - parseDatePeriod: function (expression) { - let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m); - if (matches) { - return moment().add(matches[1], matches[2]); - } - - return null; - } - -}; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js deleted file mode 100644 index f75f77ef..00000000 --- a/backend/lib/migrate_template.js +++ /dev/null @@ -1,55 +0,0 @@ -const migrate_name = 'identifier_for_migrate'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex, Promise) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - // Create Table example: - - /*return knex.schema.createTable('notification', (table) => { - table.increments().primary(); - table.string('name').notNull(); - table.string('type').notNull(); - table.integer('created_on').notNull(); - table.integer('modified_on').notNull(); - }) - .then(function () { - logger.info('[' + migrate_name + '] Notification Table created'); - });*/ - - logger.info('[' + migrate_name + '] Migrating Up Complete'); - - return Promise.resolve(true); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - // Drop table example: - - /*return knex.schema.dropTable('notification') - .then(() => { - logger.info('[' + migrate_name + '] Notification Table dropped'); - });*/ - - logger.info('[' + migrate_name + '] Migrating Down Complete'); - - return Promise.resolve(true); -}; diff --git a/backend/lib/utils.js b/backend/lib/utils.js deleted file mode 100644 index 4c8b62a8..00000000 --- a/backend/lib/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -const exec = require('child_process').exec; - -module.exports = { - - /** - * @param {String} cmd - * @returns {Promise} - */ - exec: function (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, function (err, stdout, /*stderr*/) { - if (err && typeof err === 'object') { - reject(err); - } else { - resolve(stdout.trim()); - } - }); - }); - } -}; diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js deleted file mode 100644 index 3f51b596..00000000 --- a/backend/lib/validator/api.js +++ /dev/null @@ -1,45 +0,0 @@ -const error = require('../error'); -const path = require('path'); -const parser = require('json-schema-ref-parser'); - -const ajv = require('ajv')({ - verbose: true, - validateSchema: true, - allErrors: false, - format: 'full', - coerceTypes: true -}); - -/** - * @param {Object} schema - * @param {Object} payload - * @returns {Promise} - */ -function apiValidator (schema, payload/*, description*/) { - return new Promise(function Promise_apiValidator (resolve, reject) { - if (typeof payload === 'undefined') { - reject(new error.ValidationError('Payload is undefined')); - } - - let validate = ajv.compile(schema); - let valid = validate(payload); - - if (valid && !validate.errors) { - resolve(payload); - } else { - let message = ajv.errorsText(validate.errors); - let err = new error.ValidationError(message); - err.debug = [validate.errors, payload]; - reject(err); - } - }); -} - -apiValidator.loadSchemas = parser - .dereference(path.resolve('schema/index.json')) - .then((schema) => { - ajv.addSchema(schema); - return schema; - }); - -module.exports = apiValidator; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js deleted file mode 100644 index fca6f4bf..00000000 --- a/backend/lib/validator/index.js +++ /dev/null @@ -1,49 +0,0 @@ -const _ = require('lodash'); -const error = require('../error'); -const definitions = require('../../schema/definitions.json'); - -RegExp.prototype.toJSON = RegExp.prototype.toString; - -const ajv = require('ajv')({ - verbose: true, //process.env.NODE_ENV === 'development', - allErrors: true, - format: 'full', // strict regexes for format checks - coerceTypes: true, - schemas: [ - definitions - ] -}); - -/** - * - * @param {Object} schema - * @param {Object} payload - * @returns {Promise} - */ -function validator (schema, payload) { - return new Promise(function (resolve, reject) { - if (!payload) { - reject(new error.InternalValidationError('Payload is falsy')); - } else { - try { - let validate = ajv.compile(schema); - - let valid = validate(payload); - if (valid && !validate.errors) { - resolve(_.cloneDeep(payload)); - } else { - let message = ajv.errorsText(validate.errors); - reject(new error.InternalValidationError(message)); - } - - } catch (err) { - reject(err); - } - - } - - }); - -} - -module.exports = validator; diff --git a/backend/logger.js b/backend/logger.js deleted file mode 100644 index 680af6d5..00000000 --- a/backend/logger.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Signale} = require('signale'); - -module.exports = { - global: new Signale({scope: 'Global '}), - migrate: new Signale({scope: 'Migrate '}), - express: new Signale({scope: 'Express '}), - access: new Signale({scope: 'Access '}), - nginx: new Signale({scope: 'Nginx '}), - ssl: new Signale({scope: 'SSL '}), - import: new Signale({scope: 'Importer '}), - setup: new Signale({scope: 'Setup '}), - ip_ranges: new Signale({scope: 'IP Ranges'}) -}; diff --git a/backend/migrate.js b/backend/migrate.js deleted file mode 100644 index 263c8702..00000000 --- a/backend/migrate.js +++ /dev/null @@ -1,15 +0,0 @@ -const db = require('./db'); -const logger = require('./logger').migrate; - -module.exports = { - latest: function () { - return db.migrate.currentVersion() - .then((version) => { - logger.info('Current database version:', version); - return db.migrate.latest({ - tableName: 'migrations', - directory: 'migrations' - }); - }); - } -}; diff --git a/backend/migrations/20180618015850_initial.js b/backend/migrations/20180618015850_initial.js deleted file mode 100644 index a112e826..00000000 --- a/backend/migrations/20180618015850_initial.js +++ /dev/null @@ -1,205 +0,0 @@ -const migrate_name = 'initial-schema'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('auth', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('type', 30).notNull(); - table.string('secret').notNull(); - table.json('meta').notNull(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] auth Table created'); - - return knex.schema.createTable('user', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.integer('is_disabled').notNull().unsigned().defaultTo(0); - table.string('email').notNull(); - table.string('name').notNull(); - table.string('nickname').notNull(); - table.string('avatar').notNull(); - table.json('roles').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] user Table created'); - - return knex.schema.createTable('user_permission', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('visibility').notNull(); - table.string('proxy_hosts').notNull(); - table.string('redirection_hosts').notNull(); - table.string('dead_hosts').notNull(); - table.string('streams').notNull(); - table.string('access_lists').notNull(); - table.string('certificates').notNull(); - table.unique('user_id'); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] user_permission Table created'); - - return knex.schema.createTable('proxy_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.string('forward_ip').notNull(); - table.integer('forward_port').notNull().unsigned(); - table.integer('access_list_id').notNull().unsigned().defaultTo(0); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.integer('caching_enabled').notNull().unsigned().defaultTo(0); - table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table created'); - - return knex.schema.createTable('redirection_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.string('forward_domain_name').notNull(); - table.integer('preserve_path').notNull().unsigned().defaultTo(0); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table created'); - - return knex.schema.createTable('dead_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table created'); - - return knex.schema.createTable('stream', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.integer('incoming_port').notNull().unsigned(); - table.string('forward_ip').notNull(); - table.integer('forwarding_port').notNull().unsigned(); - table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); - table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] stream Table created'); - - return knex.schema.createTable('access_list', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('name').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table created'); - - return knex.schema.createTable('certificate', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('provider').notNull(); - table.string('nice_name').notNull().defaultTo(''); - table.json('domain_names').notNull(); - table.dateTime('expires_on').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] certificate Table created'); - - return knex.schema.createTable('access_list_auth', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('access_list_id').notNull().unsigned(); - table.string('username').notNull(); - table.string('password').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list_auth Table created'); - - return knex.schema.createTable('audit_log', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('object_type').notNull().defaultTo(''); - table.integer('object_id').notNull().unsigned().defaultTo(0); - table.string('action').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] audit_log Table created'); - }); - -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20180929054513_websockets.js b/backend/migrations/20180929054513_websockets.js deleted file mode 100644 index 06054850..00000000 --- a/backend/migrations/20180929054513_websockets.js +++ /dev/null @@ -1,35 +0,0 @@ -const migrate_name = 'websockets'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); - -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; \ No newline at end of file diff --git a/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js deleted file mode 100644 index 05c27739..00000000 --- a/backend/migrations/20181019052346_forward_host.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'forward_host'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.renameColumn('forward_ip', 'forward_host'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; \ No newline at end of file diff --git a/backend/migrations/20181113041458_http2_support.js b/backend/migrations/20181113041458_http2_support.js deleted file mode 100644 index 9f6b4336..00000000 --- a/backend/migrations/20181113041458_http2_support.js +++ /dev/null @@ -1,49 +0,0 @@ -const migrate_name = 'http2_support'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; - diff --git a/backend/migrations/20181213013211_forward_scheme.js b/backend/migrations/20181213013211_forward_scheme.js deleted file mode 100644 index 22ae619e..00000000 --- a/backend/migrations/20181213013211_forward_scheme.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'forward_scheme'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.string('forward_scheme').notNull().defaultTo('http'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190104035154_disabled.js b/backend/migrations/20190104035154_disabled.js deleted file mode 100644 index 2780c4df..00000000 --- a/backend/migrations/20190104035154_disabled.js +++ /dev/null @@ -1,55 +0,0 @@ -const migrate_name = 'disabled'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('enabled').notNull().unsigned().defaultTo(1); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - - return knex.schema.table('stream', function (stream) { - stream.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] stream Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190215115310_customlocations.js b/backend/migrations/20190215115310_customlocations.js deleted file mode 100644 index 4bcfd51a..00000000 --- a/backend/migrations/20190215115310_customlocations.js +++ /dev/null @@ -1,35 +0,0 @@ -const migrate_name = 'custom_locations'; -const logger = require('../logger').migrate; - -/** - * Migrate - * Extends proxy_host table with locations field - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.json('locations'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190218060101_hsts.js b/backend/migrations/20190218060101_hsts.js deleted file mode 100644 index 648b162a..00000000 --- a/backend/migrations/20190218060101_hsts.js +++ /dev/null @@ -1,51 +0,0 @@ -const migrate_name = 'hsts'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190227065017_settings.js b/backend/migrations/20190227065017_settings.js deleted file mode 100644 index 7dc9c192..00000000 --- a/backend/migrations/20190227065017_settings.js +++ /dev/null @@ -1,38 +0,0 @@ -const migrate_name = 'settings'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('setting', (table) => { - table.string('id').notNull().primary(); - table.string('name', 100).notNull(); - table.string('description', 255).notNull(); - table.string('value', 255).notNull(); - table.json('meta').notNull(); - }) - .then(() => { - logger.info('[' + migrate_name + '] setting Table created'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20200410143839_access_list_client.js b/backend/migrations/20200410143839_access_list_client.js deleted file mode 100644 index 3511e35b..00000000 --- a/backend/migrations/20200410143839_access_list_client.js +++ /dev/null @@ -1,53 +0,0 @@ -const migrate_name = 'access_list_client'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('access_list_client', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('access_list_id').notNull().unsigned(); - table.string('address').notNull(); - table.string('directive').notNull(); - table.json('meta').notNull(); - - }) - .then(function () { - logger.info('[' + migrate_name + '] access_list_client Table created'); - - return knex.schema.table('access_list', function (access_list) { - access_list.integer('satify_any').notNull().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.dropTable('access_list_client') - .then(() => { - logger.info('[' + migrate_name + '] access_list_client Table dropped'); - }); -}; diff --git a/backend/migrations/20200410143840_access_list_client_fix.js b/backend/migrations/20200410143840_access_list_client_fix.js deleted file mode 100644 index ee0f0906..00000000 --- a/backend/migrations/20200410143840_access_list_client_fix.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'access_list_client_fix'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.renameColumn('satify_any', 'satisfy_any'); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20201014143841_pass_auth.js b/backend/migrations/20201014143841_pass_auth.js deleted file mode 100644 index a7767eb1..00000000 --- a/backend/migrations/20201014143841_pass_auth.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'pass_auth'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.integer('pass_auth').notNull().defaultTo(1); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.dropColumn('pass_auth'); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list pass_auth Column dropped'); - }); -}; diff --git a/backend/migrations/20210210154702_redirection_scheme.js b/backend/migrations/20210210154702_redirection_scheme.js deleted file mode 100644 index 0dad4876..00000000 --- a/backend/migrations/20210210154702_redirection_scheme.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'redirection_scheme'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('redirection_host', (table) => { - table.string('forward_scheme').notNull().defaultTo('$scheme'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('redirection_host', (table) => { - table.dropColumn('forward_scheme'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; diff --git a/backend/migrations/20210210154703_redirection_status_code.js b/backend/migrations/20210210154703_redirection_status_code.js deleted file mode 100644 index b9bea0b9..00000000 --- a/backend/migrations/20210210154703_redirection_status_code.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'redirection_status_code'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('redirection_host', (table) => { - table.integer('forward_http_code').notNull().unsigned().defaultTo(302); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('redirection_host', (table) => { - table.dropColumn('forward_http_code'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; diff --git a/backend/migrations/20210423103500_stream_domain.js b/backend/migrations/20210423103500_stream_domain.js deleted file mode 100644 index a894ca5e..00000000 --- a/backend/migrations/20210423103500_stream_domain.js +++ /dev/null @@ -1,40 +0,0 @@ -const migrate_name = 'stream_domain'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('stream', (table) => { - table.renameColumn('forward_ip', 'forwarding_host'); - }) - .then(function () { - logger.info('[' + migrate_name + '] stream Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('stream', (table) => { - table.renameColumn('forwarding_host', 'forward_ip'); - }) - .then(function () { - logger.info('[' + migrate_name + '] stream Table altered'); - }); -}; diff --git a/backend/migrations/20211108145214_regenerate_default_host.js b/backend/migrations/20211108145214_regenerate_default_host.js deleted file mode 100644 index 4c50941f..00000000 --- a/backend/migrations/20211108145214_regenerate_default_host.js +++ /dev/null @@ -1,50 +0,0 @@ -const migrate_name = 'stream_domain'; -const logger = require('../logger').migrate; -const internalNginx = require('../internal/nginx'); - -async function regenerateDefaultHost(knex) { - const row = await knex('setting').select('*').where('id', 'default-site').first(); - - if (!row) { - return Promise.resolve(); - } - - return internalNginx.deleteConfig('default') - .then(() => { - return internalNginx.generateConfig('default', row); - }) - .then(() => { - return internalNginx.test(); - }) - .then(() => { - return internalNginx.reload(); - }); -} - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return regenerateDefaultHost(knex); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return regenerateDefaultHost(knex); -}; \ No newline at end of file diff --git a/backend/models/access_list.js b/backend/models/access_list.js deleted file mode 100644 index 01974e86..00000000 --- a/backend/models/access_list.js +++ /dev/null @@ -1,102 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessListAuth = require('./access_list_auth'); -const AccessListClient = require('./access_list_client'); -const now = require('./now_helper'); - -Model.knex(db); - -class AccessList extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessList'; - } - - static get tableName () { - return 'access_list'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - const ProxyHost = require('./proxy_host'); - - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'access_list.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - items: { - relation: Model.HasManyRelation, - modelClass: AccessListAuth, - join: { - from: 'access_list.id', - to: 'access_list_auth.access_list_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); - } - }, - clients: { - relation: Model.HasManyRelation, - modelClass: AccessListClient, - join: { - from: 'access_list.id', - to: 'access_list_client.access_list_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); - } - }, - proxy_hosts: { - relation: Model.HasManyRelation, - modelClass: ProxyHost, - join: { - from: 'access_list.id', - to: 'proxy_host.access_list_id' - }, - modify: function (qb) { - qb.where('proxy_host.is_deleted', 0); - qb.omit(['is_deleted', 'meta']); - } - } - }; - } - - get satisfy() { - return this.satisfy_any ? 'satisfy any' : 'satisfy all'; - } - - get passauth() { - return this.pass_auth ? '' : 'proxy_set_header Authorization "";'; - } -} - -module.exports = AccessList; diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js deleted file mode 100644 index 932371f3..00000000 --- a/backend/models/access_list_auth.js +++ /dev/null @@ -1,55 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class AccessListAuth extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessListAuth'; - } - - static get tableName () { - return 'access_list_auth'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - access_list: { - relation: Model.HasOneRelation, - modelClass: require('./access_list'), - join: { - from: 'access_list_auth.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); - } - } - }; - } -} - -module.exports = AccessListAuth; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js deleted file mode 100644 index e257213a..00000000 --- a/backend/models/access_list_client.js +++ /dev/null @@ -1,59 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class AccessListClient extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessListClient'; - } - - static get tableName () { - return 'access_list_client'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - access_list: { - relation: Model.HasOneRelation, - modelClass: require('./access_list'), - join: { - from: 'access_list_client.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); - } - } - }; - } - - get rule() { - return `${this.directive} ${this.address}`; - } -} - -module.exports = AccessListClient; diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js deleted file mode 100644 index a3a318c8..00000000 --- a/backend/models/audit-log.js +++ /dev/null @@ -1,55 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class AuditLog extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AuditLog'; - } - - static get tableName () { - return 'audit_log'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - user: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'audit_log.user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'roles']); - } - } - }; - } -} - -module.exports = AuditLog; diff --git a/backend/models/auth.js b/backend/models/auth.js deleted file mode 100644 index 5ba5f380..00000000 --- a/backend/models/auth.js +++ /dev/null @@ -1,86 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const bcrypt = require('bcrypt'); -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -function encryptPassword () { - /* jshint -W040 */ - let _this = this; - - if (_this.type === 'password' && _this.secret) { - return bcrypt.hash(_this.secret, 13) - .then(function (hash) { - _this.secret = hash; - }); - } - - return null; -} - -class Auth extends Model { - $beforeInsert (queryContext) { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - return encryptPassword.apply(this, queryContext); - } - - $beforeUpdate (queryContext) { - this.modified_on = now(); - return encryptPassword.apply(this, queryContext); - } - - /** - * Verify a plain password against the encrypted password - * - * @param {String} password - * @returns {Promise} - */ - verifyPassword (password) { - return bcrypt.compare(password, this.secret); - } - - static get name () { - return 'Auth'; - } - - static get tableName () { - return 'auth'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - user: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'auth.user_id', - to: 'user.id' - }, - filter: { - is_deleted: 0 - }, - modify: function (qb) { - qb.omit(['is_deleted']); - } - } - }; - } -} - -module.exports = Auth; diff --git a/backend/models/certificate.js b/backend/models/certificate.js deleted file mode 100644 index 6084a995..00000000 --- a/backend/models/certificate.js +++ /dev/null @@ -1,73 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class Certificate extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for expires_on - if (typeof this.expires_on === 'undefined') { - this.expires_on = now(); - } - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'Certificate'; - } - - static get tableName () { - return 'certificate'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'certificate.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - } - }; - } -} - -module.exports = Certificate; diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js deleted file mode 100644 index 6de42a33..00000000 --- a/backend/models/dead_host.js +++ /dev/null @@ -1,81 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class DeadHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'DeadHost'; - } - - static get tableName () { - return 'dead_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'dead_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'dead_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = DeadHost; diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js deleted file mode 100644 index def16d08..00000000 --- a/backend/models/now_helper.js +++ /dev/null @@ -1,13 +0,0 @@ -const db = require('../db'); -const config = require('config'); -const Model = require('objection').Model; - -Model.knex(db); - -module.exports = function () { - if (config.database.knex && config.database.knex.client === 'sqlite3') { - return Model.raw('datetime(\'now\',\'localtime\')'); - } else { - return Model.raw('NOW()'); - } -}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js deleted file mode 100644 index a7583088..00000000 --- a/backend/models/proxy_host.js +++ /dev/null @@ -1,94 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessList = require('./access_list'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class ProxyHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'ProxyHost'; - } - - static get tableName () { - return 'proxy_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta', 'locations']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'proxy_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - access_list: { - relation: Model.HasOneRelation, - modelClass: AccessList, - join: { - from: 'proxy_host.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'proxy_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = ProxyHost; diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js deleted file mode 100644 index dd149b76..00000000 --- a/backend/models/redirection_host.js +++ /dev/null @@ -1,81 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class RedirectionHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'RedirectionHost'; - } - - static get tableName () { - return 'redirection_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'redirection_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'redirection_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = RedirectionHost; diff --git a/backend/models/setting.js b/backend/models/setting.js deleted file mode 100644 index 75aa9007..00000000 --- a/backend/models/setting.js +++ /dev/null @@ -1,30 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; - -Model.knex(db); - -class Setting extends Model { - $beforeInsert () { - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - static get name () { - return 'Setting'; - } - - static get tableName () { - return 'setting'; - } - - static get jsonAttributes () { - return ['meta']; - } -} - -module.exports = Setting; diff --git a/backend/models/stream.js b/backend/models/stream.js deleted file mode 100644 index ed65de0f..00000000 --- a/backend/models/stream.js +++ /dev/null @@ -1,56 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class Stream extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'Stream'; - } - - static get tableName () { - return 'stream'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'stream.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - } - }; - } -} - -module.exports = Stream; diff --git a/backend/models/token.js b/backend/models/token.js deleted file mode 100644 index 4e1b1826..00000000 --- a/backend/models/token.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - NOTE: This is not a database table, this is a model of a Token object that can be created/loaded - and then has abilities after that. - */ - -const _ = require('lodash'); -const jwt = require('jsonwebtoken'); -const crypto = require('crypto'); -const error = require('../lib/error'); -const ALGO = 'RS256'; - -let public_key = null; -let private_key = null; - -function checkJWTKeyPair() { - if (!public_key || !private_key) { - let config = require('config'); - public_key = config.get('jwt.pub'); - private_key = config.get('jwt.key'); - } -} - -module.exports = function () { - - let token_data = {}; - - let self = { - /** - * @param {Object} payload - * @returns {Promise} - */ - create: (payload) => { - // sign with RSA SHA256 - let options = { - algorithm: ALGO, - expiresIn: payload.expiresIn || '1d' - }; - - payload.jti = crypto.randomBytes(12) - .toString('base64') - .substr(-8); - - checkJWTKeyPair(); - - return new Promise((resolve, reject) => { - jwt.sign(payload, private_key, options, (err, token) => { - if (err) { - reject(err); - } else { - token_data = payload; - resolve({ - token: token, - payload: payload - }); - } - }); - }); - }, - - /** - * @param {String} token - * @returns {Promise} - */ - load: function (token) { - return new Promise((resolve, reject) => { - checkJWTKeyPair(); - try { - if (!token || token === null || token === 'null') { - reject(new error.AuthError('Empty token')); - } else { - jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => { - if (err) { - - if (err.name === 'TokenExpiredError') { - reject(new error.AuthError('Token has expired', err)); - } else { - reject(err); - } - - } else { - token_data = result; - - // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'. - // For 30 days at least, we need to replace 'all' with user. - if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) { - //console.log('Warning! Replacing "all" scope with "user"'); - - token_data.scope = ['user']; - } - - resolve(token_data); - } - }); - } - } catch (err) { - reject(err); - } - }); - - }, - - /** - * Does the token have the specified scope? - * - * @param {String} scope - * @returns {Boolean} - */ - hasScope: function (scope) { - return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1; - }, - - /** - * @param {String} key - * @return {*} - */ - get: function (key) { - if (typeof token_data[key] !== 'undefined') { - return token_data[key]; - } - - return null; - }, - - /** - * @param {String} key - * @param {*} value - */ - set: function (key, value) { - token_data[key] = value; - }, - - /** - * @param [default_value] - * @returns {Integer} - */ - getUserId: (default_value) => { - let attrs = self.get('attrs'); - if (attrs && typeof attrs.id !== 'undefined' && attrs.id) { - return attrs.id; - } - - return default_value || 0; - } - }; - - return self; -}; diff --git a/backend/models/user.js b/backend/models/user.js deleted file mode 100644 index c76f7dbf..00000000 --- a/backend/models/user.js +++ /dev/null @@ -1,56 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const UserPermission = require('./user_permission'); -const now = require('./now_helper'); - -Model.knex(db); - -class User extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for roles - if (typeof this.roles === 'undefined') { - this.roles = []; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'User'; - } - - static get tableName () { - return 'user'; - } - - static get jsonAttributes () { - return ['roles']; - } - - static get relationMappings () { - return { - permissions: { - relation: Model.HasOneRelation, - modelClass: UserPermission, - join: { - from: 'user.id', - to: 'user_permission.user_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'user_id']); - } - } - }; - } - -} - -module.exports = User; diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js deleted file mode 100644 index bb87d5dc..00000000 --- a/backend/models/user_permission.js +++ /dev/null @@ -1,29 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class UserPermission extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'UserPermission'; - } - - static get tableName () { - return 'user_permission'; - } -} - -module.exports = UserPermission; diff --git a/backend/nodemon.json b/backend/nodemon.json deleted file mode 100644 index 3d6d1342..00000000 --- a/backend/nodemon.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "verbose": false, - "ignore": [ - "data" - ], - "ext": "js json ejs" -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 28b6f178..00000000 --- a/backend/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "nginx-proxy-manager", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "js/index.js", - "dependencies": { - "ajv": "^6.12.0", - "archiver": "^5.3.0", - "batchflow": "^0.4.0", - "bcrypt": "^5.0.0", - "body-parser": "^1.19.0", - "compression": "^1.7.4", - "config": "^3.3.1", - "express": "^4.17.1", - "express-fileupload": "^1.1.9", - "gravatar": "^1.8.0", - "json-schema-ref-parser": "^8.0.0", - "jsonwebtoken": "^8.5.1", - "knex": "^0.20.13", - "liquidjs": "^9.11.10", - "lodash": "^4.17.21", - "moment": "^2.24.0", - "mysql": "^2.18.1", - "node-rsa": "^1.0.8", - "nodemon": "^2.0.2", - "objection": "^2.2.16", - "path": "^0.12.7", - "signale": "^1.4.0", - "sqlite3": "^4.1.1", - "temp-write": "^4.0.0" - }, - "signale": { - "displayDate": true, - "displayTimestamp": true - }, - "author": "Jamie Curnow ", - "license": "MIT", - "devDependencies": { - "eslint": "^6.8.0", - "eslint-plugin-align-assignments": "^1.1.2", - "prettier": "^2.0.4" - } -} diff --git a/backend/routes/api/audit-log.js b/backend/routes/api/audit-log.js deleted file mode 100644 index 8a2490c3..00000000 --- a/backend/routes/api/audit-log.js +++ /dev/null @@ -1,52 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalAuditLog = require('../../internal/audit-log'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/audit-log - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/audit-log - * - * Retrieve all logs - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalAuditLog.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js deleted file mode 100644 index 33cbbc21..00000000 --- a/backend/routes/api/main.js +++ /dev/null @@ -1,51 +0,0 @@ -const express = require('express'); -const pjson = require('../../package.json'); -const error = require('../../lib/error'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * Health Check - * GET /api - */ -router.get('/', (req, res/*, next*/) => { - let version = pjson.version.split('-').shift().split('.'); - - res.status(200).send({ - status: 'OK', - version: { - major: parseInt(version.shift(), 10), - minor: parseInt(version.shift(), 10), - revision: parseInt(version.shift(), 10) - } - }); -}); - -router.use('/schema', require('./schema')); -router.use('/tokens', require('./tokens')); -router.use('/users', require('./users')); -router.use('/audit-log', require('./audit-log')); -router.use('/reports', require('./reports')); -router.use('/settings', require('./settings')); -router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); -router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); -router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); -router.use('/nginx/streams', require('./nginx/streams')); -router.use('/nginx/access-lists', require('./nginx/access_lists')); -router.use('/nginx/certificates', require('./nginx/certificates')); - -/** - * API 404 for all other routes - * - * ALL /api/* - */ -router.all(/(.+)/, function (req, res, next) { - req.params.page = req.params['0']; - next(new error.ItemNotFoundError(req.params.page)); -}); - -module.exports = router; diff --git a/backend/routes/api/nginx/access_lists.js b/backend/routes/api/nginx/access_lists.js deleted file mode 100644 index d55c3ae1..00000000 --- a/backend/routes/api/nginx/access_lists.js +++ /dev/null @@ -1,148 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalAccessList = require('../../../internal/access-list'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/access-lists - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/access-lists - * - * Retrieve all access-lists - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalAccessList.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/access-lists - * - * Create a new access-list - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body) - .then((payload) => { - return internalAccessList.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific access-list - * - * /api/nginx/access-lists/123 - */ -router - .route('/:list_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/access-lists/123 - * - * Retrieve a specific access-list - */ - .get((req, res, next) => { - validator({ - required: ['list_id'], - additionalProperties: false, - properties: { - list_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - list_id: req.params.list_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalAccessList.get(res.locals.access, { - id: parseInt(data.list_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/access-lists/123 - * - * Update and existing access-list - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.list_id, 10); - return internalAccessList.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/access-lists/123 - * - * Delete and existing access-list - */ - .delete((req, res, next) => { - internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/certificates.js b/backend/routes/api/nginx/certificates.js deleted file mode 100644 index ffdfb515..00000000 --- a/backend/routes/api/nginx/certificates.js +++ /dev/null @@ -1,299 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalCertificate = require('../../../internal/certificate'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/certificates - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates - * - * Retrieve all certificates - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalCertificate.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/certificates - * - * Create a new certificate - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body) - .then((payload) => { - req.setTimeout(900000); // 15 minutes timeout - return internalCertificate.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Test HTTP challenge for domains - * - * /api/nginx/certificates/test-http - */ -router - .route('/test-http') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - -/** - * GET /api/nginx/certificates/test-http - * - * Test HTTP challenge for domains - */ - .get((req, res, next) => { - internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains)) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Specific certificate - * - * /api/nginx/certificates/123 - */ -router - .route('/:certificate_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates/123 - * - * Retrieve a specific certificate - */ - .get((req, res, next) => { - validator({ - required: ['certificate_id'], - additionalProperties: false, - properties: { - certificate_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - certificate_id: req.params.certificate_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalCertificate.get(res.locals.access, { - id: parseInt(data.certificate_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/certificates/123 - * - * Update and existing certificate - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.certificate_id, 10); - return internalCertificate.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/certificates/123 - * - * Update and existing certificate - */ - .delete((req, res, next) => { - internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Upload Certs - * - * /api/nginx/certificates/123/upload - */ -router - .route('/:certificate_id/upload') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/123/upload - * - * Upload certificates - */ - .post((req, res, next) => { - if (!req.files) { - res.status(400) - .send({error: 'No files were uploaded'}); - } else { - internalCertificate.upload(res.locals.access, { - id: parseInt(req.params.certificate_id, 10), - files: req.files - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - } - }); - -/** - * Renew LE Certs - * - * /api/nginx/certificates/123/renew - */ -router - .route('/:certificate_id/renew') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/123/renew - * - * Renew certificate - */ - .post((req, res, next) => { - req.setTimeout(900000); // 15 minutes timeout - internalCertificate.renew(res.locals.access, { - id: parseInt(req.params.certificate_id, 10) - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Download LE Certs - * - * /api/nginx/certificates/123/download - */ -router - .route('/:certificate_id/download') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates/123/download - * - * Renew certificate - */ - .get((req, res, next) => { - internalCertificate.download(res.locals.access, { - id: parseInt(req.params.certificate_id, 10) - }) - .then((result) => { - res.status(200) - .download(result.fileName); - }) - .catch(next); - }); - -/** - * Validate Certs before saving - * - * /api/nginx/certificates/validate - */ -router - .route('/validate') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/validate - * - * Validate certificates - */ - .post((req, res, next) => { - if (!req.files) { - res.status(400) - .send({error: 'No files were uploaded'}); - } else { - internalCertificate.validate({ - files: req.files - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - } - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/dead_hosts.js b/backend/routes/api/nginx/dead_hosts.js deleted file mode 100644 index 08b58f2d..00000000 --- a/backend/routes/api/nginx/dead_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalDeadHost = require('../../../internal/dead-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/dead-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/dead-hosts - * - * Retrieve all dead-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalDeadHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/dead-hosts - * - * Create a new dead-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalDeadHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific dead-host - * - * /api/nginx/dead-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/dead-hosts/123 - * - * Retrieve a specific dead-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalDeadHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/dead-hosts/123 - * - * Update and existing dead-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalDeadHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/dead-hosts/123 - * - * Update and existing dead-host - */ - .delete((req, res, next) => { - internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable dead-host - * - * /api/nginx/dead-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/dead-hosts/123/enable - */ - .post((req, res, next) => { - internalDeadHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable dead-host - * - * /api/nginx/dead-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/dead-hosts/123/disable - */ - .post((req, res, next) => { - internalDeadHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/proxy_hosts.js b/backend/routes/api/nginx/proxy_hosts.js deleted file mode 100644 index 6f933c3d..00000000 --- a/backend/routes/api/nginx/proxy_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalProxyHost = require('../../../internal/proxy-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/proxy-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/proxy-hosts - * - * Retrieve all proxy-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalProxyHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/proxy-hosts - * - * Create a new proxy-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalProxyHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific proxy-host - * - * /api/nginx/proxy-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/proxy-hosts/123 - * - * Retrieve a specific proxy-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalProxyHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/proxy-hosts/123 - * - * Update and existing proxy-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalProxyHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/proxy-hosts/123 - * - * Update and existing proxy-host - */ - .delete((req, res, next) => { - internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable proxy-host - * - * /api/nginx/proxy-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/proxy-hosts/123/enable - */ - .post((req, res, next) => { - internalProxyHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable proxy-host - * - * /api/nginx/proxy-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/proxy-hosts/123/disable - */ - .post((req, res, next) => { - internalProxyHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/redirection_hosts.js b/backend/routes/api/nginx/redirection_hosts.js deleted file mode 100644 index 4d44c112..00000000 --- a/backend/routes/api/nginx/redirection_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalRedirectionHost = require('../../../internal/redirection-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/redirection-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/redirection-hosts - * - * Retrieve all redirection-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/redirection-hosts - * - * Create a new redirection-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalRedirectionHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific redirection-host - * - * /api/nginx/redirection-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/redirection-hosts/123 - * - * Retrieve a specific redirection-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalRedirectionHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/redirection-hosts/123 - * - * Update and existing redirection-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalRedirectionHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/redirection-hosts/123 - * - * Update and existing redirection-host - */ - .delete((req, res, next) => { - internalRedirectionHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable redirection-host - * - * /api/nginx/redirection-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/redirection-hosts/123/enable - */ - .post((req, res, next) => { - internalRedirectionHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable redirection-host - * - * /api/nginx/redirection-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/redirection-hosts/123/disable - */ - .post((req, res, next) => { - internalRedirectionHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/streams.js b/backend/routes/api/nginx/streams.js deleted file mode 100644 index 5e3fc28f..00000000 --- a/backend/routes/api/nginx/streams.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalStream = require('../../../internal/stream'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/streams - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes - - /** - * GET /api/nginx/streams - * - * Retrieve all streams - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalStream.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/streams - * - * Create a new stream - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body) - .then((payload) => { - return internalStream.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific stream - * - * /api/nginx/streams/123 - */ -router - .route('/:stream_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes - - /** - * GET /api/nginx/streams/123 - * - * Retrieve a specific stream - */ - .get((req, res, next) => { - validator({ - required: ['stream_id'], - additionalProperties: false, - properties: { - stream_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - stream_id: req.params.stream_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalStream.get(res.locals.access, { - id: parseInt(data.stream_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/streams/123 - * - * Update and existing stream - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.stream_id, 10); - return internalStream.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/streams/123 - * - * Update and existing stream - */ - .delete((req, res, next) => { - internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable stream - * - * /api/nginx/streams/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/streams/123/enable - */ - .post((req, res, next) => { - internalStream.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable stream - * - * /api/nginx/streams/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/streams/123/disable - */ - .post((req, res, next) => { - internalStream.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/reports.js b/backend/routes/api/reports.js deleted file mode 100644 index 9e2c98c8..00000000 --- a/backend/routes/api/reports.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalReport = require('../../internal/report'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/hosts') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /reports/hosts - */ - .get(jwtdecode(), (req, res, next) => { - internalReport.getHostsReport(res.locals.access) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/schema.js b/backend/routes/api/schema.js deleted file mode 100644 index fc6bd5bd..00000000 --- a/backend/routes/api/schema.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const swaggerJSON = require('../../doc/api.swagger.json'); -const PACKAGE = require('../../package.json'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /schema - */ - .get((req, res/*, next*/) => { - let proto = req.protocol; - if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && req.headers['x-forwarded-proto']) { - proto = req.headers['x-forwarded-proto']; - } - - let origin = proto + '://' + req.hostname; - if (typeof req.headers.origin !== 'undefined' && req.headers.origin) { - origin = req.headers.origin; - } - - swaggerJSON.info.version = PACKAGE.version; - swaggerJSON.servers[0].url = origin + '/api'; - res.status(200).send(swaggerJSON); - }); - -module.exports = router; diff --git a/backend/routes/api/settings.js b/backend/routes/api/settings.js deleted file mode 100644 index d08b2bf5..00000000 --- a/backend/routes/api/settings.js +++ /dev/null @@ -1,96 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalSetting = require('../../internal/setting'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/settings - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/settings - * - * Retrieve all settings - */ - .get((req, res, next) => { - internalSetting.getAll(res.locals.access) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }); - -/** - * Specific setting - * - * /api/settings/something - */ -router - .route('/:setting_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /settings/something - * - * Retrieve a specific setting - */ - .get((req, res, next) => { - validator({ - required: ['setting_id'], - additionalProperties: false, - properties: { - setting_id: { - $ref: 'definitions#/definitions/setting_id' - } - } - }, { - setting_id: req.params.setting_id - }) - .then((data) => { - return internalSetting.get(res.locals.access, { - id: data.setting_id - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/settings/something - * - * Update and existing setting - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body) - .then((payload) => { - payload.id = req.params.setting_id; - return internalSetting.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/tokens.js b/backend/routes/api/tokens.js deleted file mode 100644 index a21f998a..00000000 --- a/backend/routes/api/tokens.js +++ /dev/null @@ -1,54 +0,0 @@ -const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalToken = require('../../internal/token'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /tokens - * - * Get a new Token, given they already have a token they want to refresh - * We also piggy back on to this method, allowing admins to get tokens - * for services like Job board and Worker. - */ - .get(jwtdecode(), (req, res, next) => { - internalToken.getFreshToken(res.locals.access, { - expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null), - scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null) - }) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }) - - /** - * POST /tokens - * - * Create a new Token - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body) - .then((payload) => { - return internalToken.getTokenFromEmail(payload); - }) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/users.js b/backend/routes/api/users.js deleted file mode 100644 index 1c6bd0ad..00000000 --- a/backend/routes/api/users.js +++ /dev/null @@ -1,239 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const userIdFromMe = require('../../lib/express/user-id-from-me'); -const internalUser = require('../../internal/user'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/users - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/users - * - * Retrieve all users - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalUser.getAll(res.locals.access, data.expand, data.query); - }) - .then((users) => { - res.status(200) - .send(users); - }) - .catch(next); - }) - - /** - * POST /api/users - * - * Create a new User - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body) - .then((payload) => { - return internalUser.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user - * - * /api/users/123 - */ -router - .route('/:user_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * GET /users/123 or /users/me - * - * Retrieve a specific user - */ - .get((req, res, next) => { - validator({ - required: ['user_id'], - additionalProperties: false, - properties: { - user_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - user_id: req.params.user_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalUser.get(res.locals.access, { - id: data.user_id, - expand: data.expand, - omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id) - }); - }) - .then((user) => { - res.status(200) - .send(user); - }) - .catch(next); - }) - - /** - * PUT /api/users/123 - * - * Update and existing user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/users/123 - * - * Update and existing user - */ - .delete((req, res, next) => { - internalUser.delete(res.locals.access, {id: req.params.user_id}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user auth - * - * /api/users/123/auth - */ -router - .route('/:user_id/auth') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * PUT /api/users/123/auth - * - * Update password for a user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.setPassword(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user permissions - * - * /api/users/123/permissions - */ -router - .route('/:user_id/permissions') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * PUT /api/users/123/permissions - * - * Set some or all permissions for a user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.setPermissions(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user login as - * - * /api/users/123/login - */ -router - .route('/:user_id/login') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/users/123/login - * - * Log in as a user - */ - .post((req, res, next) => { - internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)}) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json deleted file mode 100644 index 4b4f3405..00000000 --- a/backend/schema/definitions.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "definitions", - "definitions": { - "id": { - "description": "Unique identifier", - "example": 123456, - "readOnly": true, - "type": "integer", - "minimum": 1 - }, - "setting_id": { - "description": "Unique identifier for a Setting", - "example": "default-site", - "readOnly": true, - "type": "string", - "minLength": 2 - }, - "token": { - "type": "string", - "minLength": 10 - }, - "expand": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "sort": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "field", - "dir" - ], - "additionalProperties": false, - "properties": { - "field": { - "type": "string" - }, - "dir": { - "type": "string", - "pattern": "^(asc|desc)$" - } - } - } - }, - "query": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "string", - "minLength": 1, - "maxLength": 255 - } - ] - }, - "criteria": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "object" - } - ] - }, - "fields": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "omit": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "created_on": { - "description": "Date and time of creation", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "modified_on": { - "description": "Date and time of last update", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "user_id": { - "description": "User ID", - "example": 1234, - "type": "integer", - "minimum": 1 - }, - "certificate_id": { - "description": "Certificate ID", - "example": 1234, - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "string", - "pattern": "^new$" - } - ] - }, - "access_list_id": { - "description": "Access List ID", - "example": 1234, - "type": "integer", - "minimum": 0 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "email": { - "description": "Email Address", - "example": "john@example.com", - "format": "email", - "type": "string", - "minLength": 6, - "maxLength": 100 - }, - "password": { - "description": "Password", - "type": "string", - "minLength": 8, - "maxLength": 255 - }, - "domain_name": { - "description": "Domain Name", - "example": "jc21.com", - "type": "string", - "pattern": "^(?:[^.*]+\\.?)+[^.]$" - }, - "domain_names": { - "description": "Domain Names separated by a comma", - "example": "*.jc21.com,blog.jc21.com", - "type": "array", - "maxItems": 15, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$" - } - }, - "http_code": { - "description": "Redirect HTTP Status Code", - "example": 302, - "type": "integer", - "minimum": 300, - "maximum": 308 - }, - "scheme": { - "description": "RFC Protocol", - "example": "HTTPS or $scheme", - "type": "string", - "minLength": 4 - }, - "enabled": { - "description": "Is Enabled", - "example": true, - "type": "boolean" - }, - "ssl_enabled": { - "description": "Is SSL Enabled", - "example": true, - "type": "boolean" - }, - "ssl_forced": { - "description": "Is SSL Forced", - "example": false, - "type": "boolean" - }, - "hsts_enabled": { - "description": "Is HSTS Enabled", - "example": false, - "type": "boolean" - }, - "hsts_subdomains": { - "description": "Is HSTS applicable to all subdomains", - "example": false, - "type": "boolean" - }, - "ssl_provider": { - "type": "string", - "pattern": "^(letsencrypt|other)$" - }, - "http2_support": { - "description": "HTTP2 Protocol Support", - "example": false, - "type": "boolean" - }, - "block_exploits": { - "description": "Should we block common exploits", - "example": true, - "type": "boolean" - }, - "caching_enabled": { - "description": "Should we cache assets", - "example": true, - "type": "boolean" - } - } -} diff --git a/backend/schema/endpoints/access-lists.json b/backend/schema/endpoints/access-lists.json deleted file mode 100644 index 404e3237..00000000 --- a/backend/schema/endpoints/access-lists.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/access-lists", - "title": "Access Lists", - "description": "Endpoints relating to Access Lists", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "name": { - "type": "string", - "description": "Name of the Access List" - }, - "directive": { - "type": "string", - "enum": ["allow", "deny"] - }, - "address": { - "oneOf": [ - { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" - }, - { - "type": "string", - "pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$" - }, - { - "type": "string", - "pattern": "^all$" - } - ] - }, - "satisfy_any": { - "type": "boolean" - }, - "pass_auth": { - "type": "boolean" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "name": { - "$ref": "#/definitions/name" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Access Lists", - "href": "/nginx/access-lists", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Access List", - "href": "/nginx/access-list", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "satisfy_any": { - "$ref": "#/definitions/satisfy_any" - }, - "pass_auth": { - "$ref": "#/definitions/pass_auth" - }, - "items": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "username": { - "type": "string", - "minLength": 1 - }, - "password": { - "type": "string", - "minLength": 1 - } - } - } - }, - "clients": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "address": { - "$ref": "#/definitions/address" - }, - "directive": { - "$ref": "#/definitions/directive" - } - } - } - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Access List", - "href": "/nginx/access-list/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "satisfy_any": { - "$ref": "#/definitions/satisfy_any" - }, - "pass_auth": { - "$ref": "#/definitions/pass_auth" - }, - "items": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "username": { - "type": "string", - "minLength": 1 - }, - "password": { - "type": "string", - "minLength": 0 - } - } - } - }, - "clients": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "address": { - "$ref": "#/definitions/address" - }, - "directive": { - "$ref": "#/definitions/directive" - } - } - } - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Access List", - "href": "/nginx/access-list/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/certificates.json b/backend/schema/endpoints/certificates.json deleted file mode 100644 index 955ca75c..00000000 --- a/backend/schema/endpoints/certificates.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/certificates", - "title": "Certificates", - "description": "Endpoints relating to Certificates", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "provider": { - "$ref": "../definitions.json#/definitions/ssl_provider" - }, - "nice_name": { - "type": "string", - "description": "Nice Name for the custom certificate" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "expires_on": { - "description": "Date and time of expiration", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "meta": { - "type": "object", - "additionalProperties": false, - "properties": { - "letsencrypt_email": { - "type": "string", - "format": "email" - }, - "letsencrypt_agree": { - "type": "boolean" - }, - "dns_challenge": { - "type": "boolean" - }, - "dns_provider": { - "type": "string" - }, - "dns_provider_credentials": { - "type": "string" - }, - "propagation_seconds": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - } - ] - - } - } - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "provider": { - "$ref": "#/definitions/provider" - }, - "nice_name": { - "$ref": "#/definitions/nice_name" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "expires_on": { - "$ref": "#/definitions/expires_on" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Certificates", - "href": "/nginx/certificates", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Certificate", - "href": "/nginx/certificates", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "provider" - ], - "properties": { - "provider": { - "$ref": "#/definitions/provider" - }, - "nice_name": { - "$ref": "#/definitions/nice_name" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Certificate", - "href": "/nginx/certificates/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Test HTTP Challenge", - "description": "Tests whether the HTTP challenge should work", - "href": "/nginx/certificates/{definitions.identity.example}/test-http", - "access": "private", - "method": "GET", - "rel": "info", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - } - } - ] -} diff --git a/backend/schema/endpoints/dead-hosts.json b/backend/schema/endpoints/dead-hosts.json deleted file mode 100644 index 0c73c3be..00000000 --- a/backend/schema/endpoints/dead-hosts.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/dead-hosts", - "title": "404 Hosts", - "description": "Endpoints relating to 404 Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of 404 Hosts", - "href": "/nginx/dead-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new 404 Host", - "href": "/nginx/dead-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json deleted file mode 100644 index 9a3fff2f..00000000 --- a/backend/schema/endpoints/proxy-hosts.json +++ /dev/null @@ -1,387 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/proxy-hosts", - "title": "Proxy Hosts", - "description": "Endpoints relating to Proxy Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "forward_scheme": { - "type": "string", - "enum": ["http", "https"] - }, - "forward_host": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "forward_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "../definitions.json#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "../definitions.json#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "description": "Allow Websocket Upgrade for all paths", - "example": true, - "type": "boolean" - }, - "access_list_id": { - "$ref": "../definitions.json#/definitions/access_list_id" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - }, - "locations": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "required": [ - "forward_scheme", - "forward_host", - "forward_port", - "path" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": ["integer", "null"] - }, - "path": { - "type": "string", - "minLength": 1 - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "forward_path": { - "type": "string" - }, - "advanced_config": { - "type": "string" - } - } - } - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Proxy Hosts", - "href": "/nginx/proxy-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Proxy Host", - "href": "/nginx/proxy-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names", - "forward_scheme", - "forward_host", - "forward_port" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/redirection-hosts.json b/backend/schema/endpoints/redirection-hosts.json deleted file mode 100644 index 14a46998..00000000 --- a/backend/schema/endpoints/redirection-hosts.json +++ /dev/null @@ -1,305 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/redirection-hosts", - "title": "Redirection Hosts", - "description": "Endpoints relating to Redirection Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "../definitions.json#/definitions/http_code" - }, - "forward_scheme": { - "$ref": "../definitions.json#/definitions/scheme" - }, - "forward_domain_name": { - "$ref": "../definitions.json#/definitions/domain_name" - }, - "preserve_path": { - "description": "Should the path be preserved", - "example": true, - "type": "boolean" - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "../definitions.json#/definitions/block_exploits" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Redirection Hosts", - "href": "/nginx/redirection-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Redirection Host", - "href": "/nginx/redirection-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names", - "forward_scheme", - "forward_http_code", - "forward_domain_name" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/settings.json b/backend/schema/endpoints/settings.json deleted file mode 100644 index 29e2865a..00000000 --- a/backend/schema/endpoints/settings.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/settings", - "title": "Settings", - "description": "Endpoints relating to Settings", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/setting_id" - }, - "name": { - "description": "Name", - "example": "Default Site", - "type": "string", - "minLength": 2, - "maxLength": 100 - }, - "description": { - "description": "Description", - "example": "Default Site", - "type": "string", - "minLength": 2, - "maxLength": 255 - }, - "value": { - "description": "Value", - "example": "404", - "type": "string", - "maxLength": 255 - }, - "meta": { - "type": "object" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Settings", - "href": "/settings", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Setting", - "href": "/settings/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/value" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - } - ], - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "name": { - "$ref": "#/definitions/description" - }, - "description": { - "$ref": "#/definitions/description" - }, - "value": { - "$ref": "#/definitions/value" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } -} diff --git a/backend/schema/endpoints/streams.json b/backend/schema/endpoints/streams.json deleted file mode 100644 index 159c8036..00000000 --- a/backend/schema/endpoints/streams.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/streams", - "title": "Streams", - "description": "Endpoints relating to Streams", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "incoming_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "forwarding_host": { - "anyOf": [ - { - "$ref": "../definitions.json#/definitions/domain_name" - }, - { - "type": "string", - "format": "ipv4" - }, - { - "type": "string", - "format": "ipv6" - } - ] - }, - "forwarding_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "tcp_forwarding": { - "type": "boolean" - }, - "udp_forwarding": { - "type": "boolean" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Steams", - "href": "/nginx/streams", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Stream", - "href": "/nginx/streams", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "incoming_port", - "forwarding_host", - "forwarding_port" - ], - "properties": { - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/tokens.json b/backend/schema/endpoints/tokens.json deleted file mode 100644 index 920af63f..00000000 --- a/backend/schema/endpoints/tokens.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/tokens", - "title": "Token", - "description": "Tokens are required to authenticate against the API", - "stability": "stable", - "type": "object", - "definitions": { - "identity": { - "description": "Email Address or other 3rd party providers identifier", - "example": "john@example.com", - "type": "string" - }, - "secret": { - "description": "A password or key", - "example": "correct horse battery staple", - "type": "string" - }, - "token": { - "description": "JWT", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk", - "type": "string" - }, - "expires": { - "description": "Token expiry time", - "format": "date-time", - "type": "string" - }, - "scope": { - "description": "Scope of the Token, defaults to 'user'", - "example": "user", - "type": "string" - } - }, - "links": [ - { - "title": "Create", - "description": "Creates a new token.", - "href": "/tokens", - "access": "public", - "method": "POST", - "rel": "create", - "schema": { - "type": "object", - "required": [ - "identity", - "secret" - ], - "properties": { - "identity": { - "$ref": "#/definitions/identity" - }, - "secret": { - "$ref": "#/definitions/secret" - }, - "scope": { - "$ref": "#/definitions/scope" - } - } - }, - "targetSchema": { - "type": "object", - "properties": { - "token": { - "$ref": "#/definitions/token" - }, - "expires": { - "$ref": "#/definitions/expires" - } - } - } - }, - { - "title": "Refresh", - "description": "Returns a new token.", - "href": "/tokens", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": {}, - "targetSchema": { - "type": "object", - "properties": { - "token": { - "$ref": "#/definitions/token" - }, - "expires": { - "$ref": "#/definitions/expires" - }, - "scope": { - "$ref": "#/definitions/scope" - } - } - } - } - ] -} diff --git a/backend/schema/endpoints/users.json b/backend/schema/endpoints/users.json deleted file mode 100644 index 42f44eac..00000000 --- a/backend/schema/endpoints/users.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/users", - "title": "Users", - "description": "Endpoints relating to Users", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "name": { - "description": "Name", - "example": "Jamie Curnow", - "type": "string", - "minLength": 2, - "maxLength": 100 - }, - "nickname": { - "description": "Nickname", - "example": "Jamie", - "type": "string", - "minLength": 2, - "maxLength": 50 - }, - "email": { - "$ref": "../definitions.json#/definitions/email" - }, - "avatar": { - "description": "Avatar", - "example": "http://somewhere.jpg", - "type": "string", - "minLength": 2, - "maxLength": 150, - "readOnly": true - }, - "roles": { - "description": "Roles", - "example": [ - "admin" - ], - "type": "array" - }, - "is_disabled": { - "description": "Is Disabled", - "example": false, - "type": "boolean" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Users", - "href": "/users", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new User", - "href": "/users", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "required": [ - "name", - "nickname", - "email" - ], - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Set Password", - "description": "Sets a password for an existing User", - "href": "/users/{definitions.identity.example}/auth", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "required": [ - "type", - "secret" - ], - "properties": { - "type": { - "type": "string", - "pattern": "^password$" - }, - "current": { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 64 - } - } - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Set Permissions", - "description": "Sets Permissions for a User", - "href": "/users/{definitions.identity.example}/permissions", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "visibility": { - "type": "string", - "pattern": "^(all|user)$" - }, - "access_lists": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "dead_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "proxy_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "redirection_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "streams": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "certificates": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - } - } - }, - "targetSchema": { - "type": "boolean" - } - } - ], - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "avatar": { - "$ref": "#/definitions/avatar" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - } - } -} diff --git a/backend/schema/examples.json b/backend/schema/examples.json deleted file mode 100644 index 37bc6c4d..00000000 --- a/backend/schema/examples.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "examples", - "type": "object", - "definitions": { - "name": { - "description": "Name", - "example": "John Smith", - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "auth_header": { - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk", - "X-API-Version": "next" - }, - "token": { - "type": "string", - "description": "JWT", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk" - } - } -} diff --git a/backend/schema/index.json b/backend/schema/index.json deleted file mode 100644 index 6e7d1c8a..00000000 --- a/backend/schema/index.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "root", - "title": "Nginx Proxy Manager REST API", - "description": "This is the Nginx Proxy Manager REST API", - "version": "2.0.0", - "links": [ - { - "href": "http://npm.example.com/api", - "rel": "self" - } - ], - "properties": { - "tokens": { - "$ref": "endpoints/tokens.json" - }, - "users": { - "$ref": "endpoints/users.json" - }, - "proxy-hosts": { - "$ref": "endpoints/proxy-hosts.json" - }, - "redirection-hosts": { - "$ref": "endpoints/redirection-hosts.json" - }, - "dead-hosts": { - "$ref": "endpoints/dead-hosts.json" - }, - "streams": { - "$ref": "endpoints/streams.json" - }, - "certificates": { - "$ref": "endpoints/certificates.json" - }, - "access-lists": { - "$ref": "endpoints/access-lists.json" - }, - "settings": { - "$ref": "endpoints/settings.json" - } - } -} diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh new file mode 100755 index 00000000..bf6bf4c8 --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +BLUE='\E[1;34m' +YELLOW='\E[1;33m' +RESET='\E[0m' +RESULT=0 + +# go files: incomplete comment check +INCOMPLETE_COMMENTS=$(find . -iname "*.go*" | grep -v " " | xargs grep --colour -H -n -E "^\s*\/\/\s*[A-Z]\w+ \.{3}" 2>/dev/null) +if [[ -n "$INCOMPLETE_COMMENTS" ]]; then + echo -e "${BLUE}❯ ${YELLOW}WARN: Please fix incomplete exported comments:${RESET}" + echo -e "${RED}${INCOMPLETE_COMMENTS}${RESET}" + echo + # RESULT=1 +fi + +if ! golangci-lint run -E goimports -E maligned ./...; then + exit 1 +fi + +exit "$RESULT" diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh new file mode 100755 index 00000000..92a0a02f --- /dev/null +++ b/backend/scripts/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +export RICHGO_FORCE_COLOR=1 + +richgo test -bench=. -cover -v ./internal/... diff --git a/backend/setup.js b/backend/setup.js deleted file mode 100644 index 47fd1e7b..00000000 --- a/backend/setup.js +++ /dev/null @@ -1,233 +0,0 @@ -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); -const logger = require('./logger').setup; -const certificateModel = require('./models/certificate'); -const userModel = require('./models/user'); -const userPermissionModel = require('./models/user_permission'); -const utils = require('./lib/utils'); -const authModel = require('./models/auth'); -const settingModel = require('./models/setting'); -const dns_plugins = require('./global/certbot-dns-plugins'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; - -/** - * Creates a new JWT RSA Keypair if not alread set on the config - * - * @returns {Promise} - */ -const setupJwt = () => { - return new Promise((resolve, reject) => { - // Now go and check if the jwt gpg keys have been created and if not, create them - if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) { - logger.info('Creating a new JWT key pair...'); - - // jwt keys are not configured properly - const filename = config.util.getEnv('NODE_CONFIG_DIR') + '/' + (config.util.getEnv('NODE_ENV') || 'default') + '.json'; - let config_data = {}; - - try { - config_data = require(filename); - } catch (err) { - // do nothing - if (debug_mode) { - logger.debug(filename + ' config file could not be required'); - } - } - - // Now create the keys and save them in the config. - let key = new NodeRSA({ b: 2048 }); - key.generateKeyPair(); - - config_data.jwt = { - key: key.exportKey('private').toString(), - pub: key.exportKey('public').toString(), - }; - - // Write config - fs.writeFile(filename, JSON.stringify(config_data, null, 2), (err) => { - if (err) { - logger.error('Could not write JWT key pair to config file: ' + filename); - reject(err); - } else { - logger.info('Wrote JWT key pair to config file: ' + filename); - delete require.cache[require.resolve('config')]; - resolve(); - } - }); - } else { - // JWT key pair exists - if (debug_mode) { - logger.debug('JWT Keypair already exists'); - } - - resolve(); - } - }); -}; - -/** - * Creates a default admin users if one doesn't already exist in the database - * - * @returns {Promise} - */ -const setupDefaultUser = () => { - return userModel - .query() - .select(userModel.raw('COUNT(`id`) as `count`')) - .where('is_deleted', 0) - .first() - .then((row) => { - if (!row.count) { - // Create a new user and set password - logger.info('Creating a new user: admin@example.com with password: changeme'); - - let data = { - is_deleted: 0, - email: 'admin@example.com', - name: 'Administrator', - nickname: 'Admin', - avatar: '', - roles: ['admin'], - }; - - return userModel - .query() - .insertAndFetch(data) - .then((user) => { - return authModel - .query() - .insert({ - user_id: user.id, - type: 'password', - secret: 'changeme', - meta: {}, - }) - .then(() => { - return userPermissionModel.query().insert({ - user_id: user.id, - visibility: 'all', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage', - }); - }); - }) - .then(() => { - logger.info('Initial admin setup completed'); - }); - } else if (debug_mode) { - logger.debug('Admin user setup not required'); - } - }); -}; - -/** - * Creates default settings if they don't already exist in the database - * - * @returns {Promise} - */ -const setupDefaultSettings = () => { - return settingModel - .query() - .select(settingModel.raw('COUNT(`id`) as `count`')) - .where({id: 'default-site'}) - .first() - .then((row) => { - if (!row.count) { - settingModel - .query() - .insert({ - id: 'default-site', - name: 'Default Site', - description: 'What to show when Nginx is hit with an unknown Host', - value: 'congratulations', - meta: {}, - }) - .then(() => { - logger.info('Default settings added'); - }); - } - if (debug_mode) { - logger.debug('Default setting setup not required'); - } - }); -}; - -/** - * Installs all Certbot plugins which are required for an installed certificate - * - * @returns {Promise} - */ -const setupCertbotPlugins = () => { - return certificateModel - .query() - .where('is_deleted', 0) - .andWhere('provider', 'letsencrypt') - .then((certificates) => { - if (certificates && certificates.length) { - let plugins = []; - let promises = []; - - certificates.map(function (certificate) { - if (certificate.meta && certificate.meta.dns_challenge === true) { - const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - const packages_to_install = `${dns_plugin.package_name}${dns_plugin.version_requirement || ''} ${dns_plugin.dependencies}`; - - if (plugins.indexOf(packages_to_install) === -1) plugins.push(packages_to_install); - - // Make sure credentials file exists - const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - // Escape single quotes and backslashes - const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); - const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }'; - promises.push(utils.exec(credentials_cmd)); - } - }); - - if (plugins.length) { - const install_cmd = 'pip install ' + plugins.join(' '); - promises.push(utils.exec(install_cmd)); - } - - if (promises.length) { - return Promise.all(promises) - .then(() => { - logger.info('Added Certbot plugins ' + plugins.join(', ')); - }); - } - } - }); -}; - - -/** - * Starts a timer to call run the logrotation binary every two days - * @returns {Promise} - */ -const setupLogrotation = () => { - const intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days - - const runLogrotate = async () => { - try { - await utils.exec('logrotate /etc/logrotate.d/nginx-proxy-manager'); - logger.info('Logrotate completed.'); - } catch (e) { logger.warn(e); } - }; - - logger.info('Logrotate Timer initialized'); - setInterval(runLogrotate, intervalTimeout); - // And do this now as well - return runLogrotate(); -}; - -module.exports = function () { - return setupJwt() - .then(setupDefaultUser) - .then(setupDefaultSettings) - .then(setupCertbotPlugins) - .then(setupLogrotation); -}; diff --git a/backend/templates/_assets.conf b/backend/templates/_assets.conf deleted file mode 100644 index dcb183c5..00000000 --- a/backend/templates/_assets.conf +++ /dev/null @@ -1,4 +0,0 @@ -{% if caching_enabled == 1 or caching_enabled == true -%} - # Asset Caching - include conf.d/include/assets.conf; -{% endif %} \ No newline at end of file diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf deleted file mode 100644 index 06ca7bb8..00000000 --- a/backend/templates/_certificates.conf +++ /dev/null @@ -1,14 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if certificate.provider == "letsencrypt" %} - # Let's Encrypt SSL - include conf.d/include/letsencrypt-acme-challenge.conf; - include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; -{% else %} - # Custom SSL - ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem; -{% endif %} -{% endif %} - diff --git a/backend/templates/_exploits.conf b/backend/templates/_exploits.conf deleted file mode 100644 index 002970d5..00000000 --- a/backend/templates/_exploits.conf +++ /dev/null @@ -1,4 +0,0 @@ -{% if block_exploits == 1 or block_exploits == true %} - # Block Exploits - include conf.d/include/block-exploits.conf; -{% endif %} \ No newline at end of file diff --git a/backend/templates/_forced_ssl.conf b/backend/templates/_forced_ssl.conf deleted file mode 100644 index 7fade20c..00000000 --- a/backend/templates/_forced_ssl.conf +++ /dev/null @@ -1,6 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} - # Force SSL - include conf.d/include/force-ssl.conf; -{% endif %} -{% endif %} \ No newline at end of file diff --git a/backend/templates/_header_comment.conf b/backend/templates/_header_comment.conf deleted file mode 100644 index 8f996d34..00000000 --- a/backend/templates/_header_comment.conf +++ /dev/null @@ -1,3 +0,0 @@ -# ------------------------------------------------------------ -# {{ domain_names | join: ", " }} -# ------------------------------------------------------------ \ No newline at end of file diff --git a/backend/templates/_hsts.conf b/backend/templates/_hsts.conf deleted file mode 100644 index 11aecf24..00000000 --- a/backend/templates/_hsts.conf +++ /dev/null @@ -1,8 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} -{% if hsts_enabled == 1 or hsts_enabled == true %} - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always; -{% endif %} -{% endif %} -{% endif %} diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf deleted file mode 100644 index 730f3a7c..00000000 --- a/backend/templates/_listen.conf +++ /dev/null @@ -1,15 +0,0 @@ - listen 80; -{% if ipv6 -%} - listen [::]:80; -{% else -%} - #listen [::]:80; -{% endif %} -{% if certificate -%} - listen 443 ssl{% if http2_support %} http2{% endif %}; -{% if ipv6 -%} - listen [::]:443 ssl{% if http2_support %} http2{% endif %}; -{% else -%} - #listen [::]:443; -{% endif %} -{% endif %} - server_name {{ domain_names | join: " " }}; diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf deleted file mode 100644 index 5a7a6abe..00000000 --- a/backend/templates/_location.conf +++ /dev/null @@ -1,45 +0,0 @@ - location {{ path }} { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; - - {% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{ access_list_id }}; - - {{ access_list.passauth }} - {% endif %} - - # Access Rules - {% for client in access_list.clients %} - {{- client.rule -}}; - {% endfor %}deny all; - - # Access checks must... - {% if access_list.satisfy %} - {{ access_list.satisfy }}; - {% endif %} - - {% endif %} - - {% include "_assets.conf" %} - {% include "_exploits.conf" %} - - {% include "_forced_ssl.conf" %} - {% include "_hsts.conf" %} - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - {% endif %} - - - {{ advanced_config }} - } - diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf deleted file mode 100644 index d94dff57..00000000 --- a/backend/templates/dead_host.conf +++ /dev/null @@ -1,23 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - - access_log /data/logs/dead-host-{{ id }}_access.log standard; - error_log /data/logs/dead-host-{{ id }}_error.log warn; - -{{ advanced_config }} - -{% if use_default_location %} - location / { -{% include "_hsts.conf" %} - return 404; - } -{% endif %} - -} -{% endif %} diff --git a/backend/templates/default.conf b/backend/templates/default.conf deleted file mode 100644 index ec68530c..00000000 --- a/backend/templates/default.conf +++ /dev/null @@ -1,40 +0,0 @@ -# ------------------------------------------------------------ -# Default Site -# ------------------------------------------------------------ -{% if value == "congratulations" %} -# Skipping output, congratulations page configration is baked in. -{%- else %} -server { - listen 80 default; -{% if ipv6 -%} - listen [::]:80 default; -{% else -%} - #listen [::]:80 default; -{% endif %} - server_name default-host.localhost; - access_log /data/logs/default-host_access.log combined; - error_log /data/logs/default-host_error.log warn; -{% include "_exploits.conf" %} - - include conf.d/include/letsencrypt-acme-challenge.conf; - -{%- if value == "404" %} - location / { - return 404; - } -{% endif %} - -{%- if value == "redirect" %} - location / { - return 301 {{ meta.redirect }}; - } -{%- endif %} - -{%- if value == "html" %} - root /data/nginx/default_www; - location / { - try_files $uri /index.html; - } -{%- endif %} -} -{% endif %} diff --git a/backend/templates/ip_ranges.conf b/backend/templates/ip_ranges.conf deleted file mode 100644 index 8ede2bd9..00000000 --- a/backend/templates/ip_ranges.conf +++ /dev/null @@ -1,3 +0,0 @@ -{% for range in ip_ranges %} -set_real_ip_from {{ range }}; -{% endfor %} \ No newline at end of file diff --git a/backend/templates/letsencrypt-request.conf b/backend/templates/letsencrypt-request.conf deleted file mode 100644 index 676c8a60..00000000 --- a/backend/templates/letsencrypt-request.conf +++ /dev/null @@ -1,19 +0,0 @@ -{% include "_header_comment.conf" %} - -server { - listen 80; -{% if ipv6 -%} - listen [::]:80; -{% endif %} - - server_name {{ domain_names | join: " " }}; - - access_log /data/logs/letsencrypt-requests_access.log standard; - error_log /data/logs/letsencrypt-requests_error.log warn; - - include conf.d/include/letsencrypt-acme-challenge.conf; - - location / { - return 404; - } -} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf deleted file mode 100644 index ec30cca0..00000000 --- a/backend/templates/proxy_host.conf +++ /dev/null @@ -1,70 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { - set $forward_scheme {{ forward_scheme }}; - set $server "{{ forward_host }}"; - set $port {{ forward_port }}; - -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_assets.conf" %} -{% include "_exploits.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - -{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection $http_connection; -proxy_http_version 1.1; -{% endif %} - - access_log /data/logs/proxy-host-{{ id }}_access.log proxy; - error_log /data/logs/proxy-host-{{ id }}_error.log warn; - -{{ advanced_config }} - -{{ locations }} - -{% if use_default_location %} - - location / { - - {% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{ access_list_id }}; - - {{ access_list.passauth }} - {% endif %} - - # Access Rules - {% for client in access_list.clients %} - {{- client.rule -}}; - {% endfor %}deny all; - - # Access checks must... - {% if access_list.satisfy %} - {{ access_list.satisfy }}; - {% endif %} - - {% endif %} - -{% include "_hsts.conf" %} - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - {% endif %} - - # Proxy! - include conf.d/include/proxy.conf; - } -{% endif %} - - # Custom - include /data/nginx/custom/server_proxy[.]conf; -} -{% endif %} diff --git a/backend/templates/redirection_host.conf b/backend/templates/redirection_host.conf deleted file mode 100644 index 339fe72e..00000000 --- a/backend/templates/redirection_host.conf +++ /dev/null @@ -1,32 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_assets.conf" %} -{% include "_exploits.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - - access_log /data/logs/redirection-host-{{ id }}_access.log standard; - error_log /data/logs/redirection-host-{{ id }}_error.log warn; - -{{ advanced_config }} - -{% if use_default_location %} - location / { -{% include "_hsts.conf" %} - - {% if preserve_path == 1 or preserve_path == true %} - return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri; - {% else %} - return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}; - {% endif %} - } -{% endif %} - - # Custom - include /data/nginx/custom/server_redirect[.]conf; -} -{% endif %} diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf deleted file mode 100644 index 76159a64..00000000 --- a/backend/templates/stream.conf +++ /dev/null @@ -1,37 +0,0 @@ -# ------------------------------------------------------------ -# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} -# ------------------------------------------------------------ - -{% if enabled %} -{% if tcp_forwarding == 1 or tcp_forwarding == true -%} -server { - listen {{ incoming_port }}; -{% if ipv6 -%} - listen [::]:{{ incoming_port }}; -{% else -%} - #listen [::]:{{ incoming_port }}; -{% endif %} - - proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; - - # Custom - include /data/nginx/custom/server_stream[.]conf; - include /data/nginx/custom/server_stream_tcp[.]conf; -} -{% endif %} -{% if udp_forwarding == 1 or udp_forwarding == true %} -server { - listen {{ incoming_port }} udp; -{% if ipv6 -%} - listen [::]:{{ incoming_port }} udp; -{% else -%} - #listen [::]:{{ incoming_port }} udp; -{% endif %} - proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; - - # Custom - include /data/nginx/custom/server_stream[.]conf; - include /data/nginx/custom/server_stream_udp[.]conf; -} -{% endif %} -{% endif %} \ No newline at end of file diff --git a/backend/yarn.lock b/backend/yarn.lock deleted file mode 100644 index 96883182..00000000 --- a/backend/yarn.lock +++ /dev/null @@ -1,3754 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@apidevtools/json-schema-ref-parser@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#9eb749499b3f8d919e90bb141e4b6f67aee4692d" - integrity sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw== - dependencies: - "@jsdevtools/ono" "^7.1.0" - call-me-maybe "^1.0.1" - js-yaml "^3.13.1" - -"@babel/code-frame@^7.0.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@jsdevtools/ono@^7.1.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== - -acorn@^7.1.1: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== - -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.6: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-align@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== - dependencies: - string-width "^3.0.0" - -ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== - dependencies: - type-fest "^0.11.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - -archiver@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" - integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batchflow@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/batchflow/-/batchflow-0.4.0.tgz#7d419df79b6b7587b06f9ea34f96ccef6f74e5b5" - integrity sha1-fUGd95trdYewb56jT5bM72905bU= - -bcrypt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.0.tgz#051407c7cd5ffbfb773d541ca3760ea0754e37e2" - integrity sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg== - dependencies: - node-addon-api "^3.0.0" - node-pre-gyp "0.15.0" - -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -blueimp-md5@^2.16.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.17.0.tgz#f4fcac088b115f7b4045f19f5da59e9d01b1bb96" - integrity sha512-x5PKJHY5rHQYaADj6NwPUR2QRCUVSggPzrUKkeENpj871o9l9IefJbO2jkT5UvYykeOK9dx0VmkIo6dZ+vThYw== - -body-parser@1.19.0, body-parser@^1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -boxen@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" - widest-line "^3.1.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -busboy@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" - integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== - dependencies: - dicer "0.3.0" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chokidar@^3.2.2: - version "3.4.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" - integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.4.0" - optionalDependencies: - fsevents "~2.1.2" - -chownr@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cli-boxes@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" - integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7" - integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg== - -commander@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compress-commons@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" - integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.2" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -config@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/config/-/config-3.3.1.tgz#b6a70e2908a43b98ed20be7e367edf0cc8ed5a19" - integrity sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q== - dependencies: - json5 "^2.1.1" - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -crc-32@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" - integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" - -crc32-stream@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" - integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== - dependencies: - crc-32 "^1.2.0" - readable-stream "^3.4.0" - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -db-errors@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2" - integrity sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng== - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4.1.1, debug@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -dicer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== - dependencies: - is-obj "^2.0.0" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -email-validator@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" - integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-plugin-align-assignments@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-align-assignments/-/eslint-plugin-align-assignments-1.1.2.tgz#83e1a8a826d4adf29e82b52d0bb39c88b301b576" - integrity sha512-I1ZJgk9EjHfGVU9M2Ex8UkVkkjLL5Y9BS6VNnQHq79eHj2H4/Cgxf36lQSUTLgm2ntB03A2NtF+zg9fyi5vChg== - -eslint-scope@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint@^6.8.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^7.0.0" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.3" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -esm@^3.2.25: - version "3.2.25" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== - dependencies: - acorn "^7.1.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.1.0, estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -express-fileupload@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.1.9.tgz#e798e9318394ed5083e56217ad6cda576da465d2" - integrity sha512-f2w0aoe7lj3NeD8a4MXmYQsqir3Z66I08l9AKq04QbFUAjeZNmPwTlR5Lx2NGwSu/PslsAjGC38MWzo5tTjoBg== - dependencies: - busboy "^0.3.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" - integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== - dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -flagged-respawn@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" - integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-minipass@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" - integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getopts@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" - integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== - -glob-parent@^5.0.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.4: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" - integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== - dependencies: - ini "^1.3.5" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -gravatar@^1.8.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.8.1.tgz#743bbdf3185c3433172e00e0e6ff5f6b30c58997" - integrity sha512-18frnfVp4kRYkM/eQW32Mfwlsh/KMbwd3S6nkescBZHioobflFEFHsvM71qZAkUSLNifyi2uoI+TuGxJAnQIOA== - dependencies: - blueimp-md5 "^2.16.0" - email-validator "^2.0.4" - querystring "0.2.0" - yargs "^15.4.1" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= - -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inquirer@^7.0.0: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -interpret@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-ref-parser@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#7c758fac2cf822c05e837abd0a13f8fa2c15ffd4" - integrity sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ== - dependencies: - "@apidevtools/json-schema-ref-parser" "8.0.0" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json5@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -knex@^0.20.13: - version "0.20.15" - resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.15.tgz#b7e9e1efd9cf35d214440d9439ed21153574679d" - integrity sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q== - dependencies: - colorette "1.1.0" - commander "^4.1.1" - debug "4.1.1" - esm "^3.2.25" - getopts "2.2.5" - inherits "~2.0.4" - interpret "^2.0.0" - liftoff "3.1.0" - lodash "^4.17.15" - mkdirp "^0.5.1" - pg-connection-string "2.1.0" - tarn "^2.0.0" - tildify "2.0.0" - uuid "^7.0.1" - v8flags "^3.1.3" - -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -liftoff@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" - integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== - dependencies: - extend "^3.0.0" - findup-sync "^3.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" - -liquidjs@^9.11.10: - version "9.15.0" - resolved "https://registry.yarnpkg.com/liquidjs/-/liquidjs-9.15.0.tgz#03e8c13aeda89801a346c614b0802f320458d0ac" - integrity sha512-wRPNfMx6X3GGEDqTlBpw7VMo8ylKkzLYTcd7eeaDeYnZyR5BqUgF9tZy3FdPCHV2N/BassGKmlmlpJiRXGFOqg== - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-iterator@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" - integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== - dependencies: - kind-of "^6.0.2" - -map-cache@^0.2.0, map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.0.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^2.6.0, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -moment@^2.24.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -mysql@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== - dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" - -nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -needle@^2.2.1, needle@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.0.tgz#e6fc4b3cc6c25caed7554bd613a5cf0bac8c31c0" - integrity sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-addon-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" - integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== - -node-pre-gyp@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087" - integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.3" - needle "^2.5.0" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4.4.2" - -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -node-rsa@^1.0.8: - version "1.1.1" - resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" - integrity sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw== - dependencies: - asn1 "^0.2.4" - -nodemon@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" - integrity sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ== - dependencies: - chokidar "^3.2.2" - debug "^3.2.6" - ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.7" - semver "^5.7.1" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.2" - update-notifier "^4.0.0" - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.pick@^1.2.0, object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -objection@^2.2.16: - version "2.2.16" - resolved "https://registry.yarnpkg.com/objection/-/objection-2.2.16.tgz#552ec6d625a7f80d6e204fc63732cbd3fc56f31c" - integrity sha512-sq8erZdxW5ruPUK6tVvwDxyO16U49XAn/BmOm2zaNhNA2phOPCe2/7+R70nDEF1SFrgJOrwDu/PtoxybuJxnjQ== - dependencies: - ajv "^6.12.6" - db-errors "^0.2.3" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.1.tgz#5c8016847b0d67fcedb7eef254751cfcdc7e9418" - integrity sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-filepath@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= - dependencies: - path-root-regex "^0.1.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path@^0.12.7: - version "0.12.7" - resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" - integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= - dependencies: - process "^0.11.1" - util "^0.10.3" - -pg-connection-string@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502" - integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pkg-conf@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" - integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= - dependencies: - find-up "^2.0.0" - load-json-file "^4.0.0" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== - -printj@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" - integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.1: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -pupa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" - integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== - dependencies: - escape-goat "^2.0.0" - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@2.3.7, readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdir-glob@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" - integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== - dependencies: - minimatch "^3.0.4" - -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -registry-auth-token@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" - integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6, resolve@^1.1.7: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -rxjs@^6.6.0: - version "6.6.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" - integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -signale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/signale/-/signale-1.4.0.tgz#c4be58302fb0262ac00fc3d886a7c113759042f1" - integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== - dependencies: - chalk "^2.3.2" - figures "^2.0.0" - pkg-conf "^2.1.0" - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sqlite3@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.2.0.tgz#49026d665e9fc4f922e56fb9711ba5b4c85c4901" - integrity sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg== - dependencies: - nan "^2.12.1" - node-pre-gyp "^0.11.0" - -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-json-comments@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^5.3.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -tar-stream@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^4, tar@^4.4.2: - version "4.4.19" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" - integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== - dependencies: - chownr "^1.1.4" - fs-minipass "^1.2.7" - minipass "^2.9.0" - minizlib "^1.3.3" - mkdirp "^0.5.5" - safe-buffer "^5.2.1" - yallist "^3.1.1" - -tarn@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tarn/-/tarn-2.0.0.tgz#c68499f69881f99ae955b4317ca7d212d942fdee" - integrity sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA== - -temp-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" - integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= - -temp-write@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" - integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== - dependencies: - graceful-fs "^4.1.15" - is-stream "^2.0.0" - make-dir "^3.0.0" - temp-dir "^1.0.0" - uuid "^3.3.2" - -term-size@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" - integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tildify@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" - integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= - -undefsafe@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" - integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== - dependencies: - debug "^2.2.0" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -update-notifier@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" - integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== - dependencies: - boxen "^4.2.0" - chalk "^3.0.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@^0.10.3: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== - dependencies: - inherits "2.0.3" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - -v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== - -v8flags@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.14, which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^3.0.0, yallist@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0" diff --git a/docker/Dockerfile b/docker/Dockerfile index 378fffbf..3a0c7b4f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,63 +1,107 @@ # This is a Dockerfile intended to be built using `docker buildx` # for multi-arch support. Building with `docker build` may have unexpected results. -# This file assumes that the frontend has been built using ./scripts/frontend-build +# This file assumes that these scripts have been run first: +# - ./scripts/ci/build-frontend -FROM nginxproxymanager/nginx-full:certbot-node +FROM nginxproxymanager/testca as testca +FROM letsencrypt/pebble as pebbleca +FROM jc21/gotools:latest AS gobuild -ARG TARGETPLATFORM +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG BUILD_COMMIT +ARG BUILD_VERSION +ARG GOPRIVATE +ARG GOPROXY +ARG SENTRY_DSN + +ENV BUILD_COMMIT="${BUILD_COMMIT:-dev}" \ + BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \ + CGO_ENABLED=1 \ + GO111MODULE=on \ + GOPRIVATE="${GOPRIVATE:-}" \ + GOPROXY="${GOPROXY:-}" \ + SENTRY_DSN="${SENTRY_DSN:-}" + +COPY scripts /scripts +COPY backend /app +WORKDIR /app + +RUN mkdir -p /dist \ + && /scripts/go-multiarch-wrapper /dist/server + +#=============== +# Final image +#=============== + +FROM nginxproxymanager/nginx-full:acmesh AS final + +COPY --from=gobuild /dist/server /app/bin/server +# these certs are used for testing in CI +COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt + +# These acmesh vars are defined in the base image +ENV SUPPRESS_NO_CONFIG_WARNING=1 \ + S6_FIX_ATTRS_HIDDEN=1 \ + ACMESH_CONFIG_HOME=/data/.acme.sh/config \ + ACMESH_HOME=/data/.acme.sh \ + CERT_HOME=/data/.acme.sh/certs \ + LE_CONFIG_HOME=/data/.acme.sh/config \ + LE_WORKING_DIR=/data/.acme.sh + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf + +# s6 overlay +COPY scripts/install-s6 /tmp/install-s6 +RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -rf /tmp/* + +EXPOSE 80/tcp 81/tcp 443/tcp + +COPY docker/rootfs / + +# Remove frontend service not required for prod, dev nginx config as well +# and remove any other cruft +RUN rm -rf /etc/services.d/frontend \ + /etc/nginx/conf.d/dev.conf \ + /var/cache/* \ + /var/log/* \ + /tmp/* \ + /var/lib/dpkg/status-old + +# Dummy cert +RUN openssl req \ + -new \ + -newkey rsa:2048 \ + -days 3650 \ + -nodes \ + -x509 \ + -subj '/O=Nginx Proxy Manager/OU=Dummy Certificate/CN=localhost' \ + -keyout /etc/ssl/certs/dummykey.pem \ + -out /etc/ssl/certs/dummycert.pem \ + && chmod +r /etc/ssl/certs/dummykey.pem /etc/ssl/certs/dummycert.pem + +VOLUME /data + +CMD [ "/init" ] + +ARG NOW ARG BUILD_VERSION ARG BUILD_COMMIT ARG BUILD_DATE -ENV SUPPRESS_NO_CONFIG_WARNING=1 \ - S6_FIX_ATTRS_HIDDEN=1 \ - S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ - NODE_ENV=production \ - NPM_BUILD_VERSION="${BUILD_VERSION}" \ - NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ - NPM_BUILD_DATE="${BUILD_DATE}" - -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ - && apt-get update \ - && apt-get install -y --no-install-recommends jq logrotate \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# s6 overlay -COPY scripts/install-s6 /tmp/install-s6 -RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 - -EXPOSE 80 81 443 - -COPY backend /app -COPY frontend/dist /app/frontend -COPY global /app/global - -WORKDIR /app -RUN yarn install - -# add late to limit cache-busting by modifications -COPY docker/rootfs / - -# Remove frontend service not required for prod, dev nginx config as well -RUN rm -rf /etc/services.d/frontend /etc/nginx/conf.d/dev.conf - -# Change permission of logrotate config file -RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager - -# fix for pip installs -# https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1769 -RUN pip uninstall --yes setuptools \ - && pip install "setuptools==58.0.0" - -VOLUME [ "/data", "/etc/letsencrypt" ] -ENTRYPOINT [ "/init" ] +ENV NPM_BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \ + NPM_BUILD_COMMIT="${BUILD_COMMIT:-dev}" \ + NPM_BUILD_DATE="${BUILD_DATE:-}" LABEL org.label-schema.schema-version="1.0" \ org.label-schema.license="MIT" \ org.label-schema.name="nginx-proxy-manager" \ - org.label-schema.description="Docker container for managing Nginx proxy hosts with a simple, powerful interface " \ - org.label-schema.url="https://github.com/jc21/nginx-proxy-manager" \ - org.label-schema.vcs-url="https://github.com/jc21/nginx-proxy-manager.git" \ - org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:latest" + org.label-schema.description="Nginx Host Management and Proxy" \ + org.label-schema.build-date="${NOW:-}" \ + org.label-schema.version="${BUILD_VERSION:-0.0.0}" \ + org.label-schema.url="https://nginxproxymanager.com" \ + org.label-schema.vcs-url="https://github.com/NginxProxyManager/nginx-proxy-manager.git" \ + org.label-schema.vcs-ref="${BUILD_COMMIT:-dev}" \ + org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:${BUILD_VERSION:-0.0.0}" diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index d2e2266a..4b0fe429 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,15 +1,34 @@ -FROM nginxproxymanager/nginx-full:certbot-node +FROM nginxproxymanager/testca as testca +FROM letsencrypt/pebble as pebbleca +FROM nginxproxymanager/nginx-full:acmesh-golang LABEL maintainer="Jamie Curnow " -ENV S6_LOGGING=0 \ - SUPPRESS_NO_CONFIG_WARNING=1 \ - S6_FIX_ATTRS_HIDDEN=1 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ +ARG GOPROXY +ARG GOPRIVATE + +ENV GOPROXY=$GOPROXY \ + GOPRIVATE=$GOPRIVATE \ + S6_LOGGING=0 \ + SUPPRESS_NO_CONFIG_WARNING=1 \ + S6_FIX_ATTRS_HIDDEN=1 \ + ACMESH_CONFIG_HOME=/data/.acme.sh/config \ + ACMESH_HOME=/data/.acme.sh \ + CERT_HOME=/data/.acme.sh/certs \ + LE_CONFIG_HOME=/data/.acme.sh/config \ + LE_WORKING_DIR=/data/.acme.sh + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf + +# usql and node +RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \ && apt-get update \ - && apt-get install -y certbot jq python3-pip logrotate \ + && apt-get install -y --no-install-recommends nodejs vim dnsutils \ + && npm install -g yarn \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && go install github.com/xo/usql@master # Task RUN cd /usr \ @@ -18,12 +37,29 @@ RUN cd /usr \ COPY rootfs / RUN rm -f /etc/nginx/conf.d/production.conf -RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager # s6 overlay RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz" \ && tar -xzf /tmp/s6-overlay-amd64.tar.gz -C / -EXPOSE 80 81 443 -ENTRYPOINT [ "/init" ] +# Fix for golang dev: +RUN chown -R 1000:1000 /opt/go +COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt + +# Dummy cert +RUN openssl req \ + -new \ + -newkey rsa:2048 \ + -days 3650 \ + -nodes \ + -x509 \ + -subj '/O=Nginx Proxy Manager/OU=Dummy Certificate/CN=localhost' \ + -keyout /etc/ssl/certs/dummykey.pem \ + -out /etc/ssl/certs/dummycert.pem \ + && chmod +r /etc/ssl/certs/dummykey.pem /etc/ssl/certs/dummycert.pem + +EXPOSE 80 +CMD [ "/init" ] +HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://127.0.0.1:81/api || exit 1 diff --git a/docker/dev/dnsrouter-config.json b/docker/dev/dnsrouter-config.json new file mode 100644 index 00000000..9560d7b8 --- /dev/null +++ b/docker/dev/dnsrouter-config.json @@ -0,0 +1,28 @@ +{ + "log": { + "format": "nice", + "level": "debug" + }, + "servers": [ + { + "host": "0.0.0.0", + "port": 53, + "upstreams": [ + { + "regex": "website[0-9]+.example\\.com", + "upstream": "127.0.0.11" + }, + { + "regex": ".*\\.example\\.com", + "upstream": "1.1.1.1" + }, + { + "regex": "local", + "nxdomain": true + } + ], + "internal": null, + "default_upstream": "127.0.0.11" + } + ] +} \ No newline at end of file diff --git a/docker/dev/pdns-db.sql b/docker/dev/pdns-db.sql new file mode 100644 index 00000000..dd7f293a --- /dev/null +++ b/docker/dev/pdns-db.sql @@ -0,0 +1,87 @@ +CREATE TABLE `comments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `type` varchar(10) NOT NULL, + `modified_at` int(11) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + `comment` text CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`id`), + KEY `comments_name_type_idx` (`name`,`type`), + KEY `comments_order_idx` (`domain_id`,`modified_at`) +); + +CREATE TABLE `cryptokeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `flags` int(11) NOT NULL, + `active` tinyint(1) DEFAULT NULL, + `published` tinyint(1) DEFAULT 1, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainidindex` (`domain_id`) +); + +CREATE TABLE `domainmetadata` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `kind` varchar(32) DEFAULT NULL, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainmetadata_idx` (`domain_id`,`kind`) +); + +INSERT INTO `domainmetadata` VALUES (1,1,'SOA-EDIT-API','DEFAULT'); + +CREATE TABLE `domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `master` varchar(128) DEFAULT NULL, + `last_check` int(11) DEFAULT NULL, + `type` varchar(6) NOT NULL, + `notified_serial` int(10) unsigned DEFAULT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name_index` (`name`) +); + +INSERT INTO `domains` VALUES (1,'example.com','',NULL,'NATIVE',NULL,''); + +CREATE TABLE `records` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `type` varchar(10) DEFAULT NULL, + `content` TEXT DEFAULT NULL, + `ttl` int(11) DEFAULT NULL, + `prio` int(11) DEFAULT NULL, + `disabled` tinyint(1) DEFAULT 0, + `ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `auth` tinyint(1) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `nametype_index` (`name`,`type`), + KEY `domain_id` (`domain_id`), + KEY `ordername` (`ordername`) +); + +INSERT INTO `records` VALUES +(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1), +(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1), +(4,1,'test.example.com','A','10.0.0.1',60,0,0,NULL,1), +(5,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2022020702 10800 3600 604800 3600',1500,0,0,NULL,1); + +CREATE TABLE `supermasters` ( + `ip` varchar(64) NOT NULL, + `nameserver` varchar(255) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`ip`,`nameserver`) +); + +CREATE TABLE `tsigkeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `algorithm` varchar(50) DEFAULT NULL, + `secret` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `namealgoindex` (`name`,`algorithm`) +); diff --git a/docker/dev/pebble-config.json b/docker/dev/pebble-config.json new file mode 100644 index 00000000..289d2906 --- /dev/null +++ b/docker/dev/pebble-config.json @@ -0,0 +1,12 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:443", + "managementListenAddress": "0.0.0.0:15000", + "certificate": "test/certs/localhost/cert.pem", + "privateKey": "test/certs/localhost/key.pem", + "httpPort": 80, + "tlsPort": 443, + "ocspResponderURL": "", + "externalAccountBindingRequired": false + } +} \ No newline at end of file diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index a8049ec8..90164d2b 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -1,80 +1,91 @@ # WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. -version: "3" +version: "3.8" services: - fullstack-mysql: - image: ${IMAGE}:ci-${BUILD_NUMBER} + fullstack: + image: ${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER} environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" + - NPM_LOG_LEVEL=debug volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 + - '/etc/localtime:/etc/localtime:ro' + - npm_data_ci:/data + - ../docs:/temp-docs + - ./dev/resolv.conf:/etc/resolv.conf:ro + networks: + default: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com + + stepca: + image: nginxproxymanager/testca + volumes: + - ./dev/resolv.conf:/etc/resolv.conf:ro + - '/etc/localtime:/etc/localtime:ro' + networks: + default: + aliases: + - ca.internal + + pdns: + image: pschiffe/pdns-mysql + volumes: + - '/etc/localtime:/etc/localtime:ro' + environment: + PDNS_master: 'yes' + PDNS_api: 'yes' + PDNS_api_key: 'npm' + PDNS_webserver: 'yes' + PDNS_webserver_address: '0.0.0.0' + PDNS_webserver_password: 'npm' + PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_version_string: 'anonymous' + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_gmysql_host: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns depends_on: - - db - healthcheck: - test: ["CMD", "/bin/check-health"] - interval: 10s - timeout: 3s + - pdns-db + networks: + default: + aliases: + - ns1.pdns + - ns2.pdns - fullstack-sqlite: - image: ${IMAGE}:ci-${BUILD_NUMBER} + pdns-db: + image: mariadb environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DB_SQLITE_FILE: "/data/database.sqlite" + MYSQL_ROOT_PASSWORD: 'pdns' + MYSQL_DATABASE: 'pdns' + MYSQL_USER: 'pdns' + MYSQL_PASSWORD: 'pdns' volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 - healthcheck: - test: ["CMD", "/bin/check-health"] - interval: 10s - timeout: 3s + - pdns_mysql_vol:/var/lib/mysql + - '/etc/localtime:/etc/localtime:ro' + - ./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro - db: - image: jc21/mariadb-aria - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" + dnsrouter: + image: jc21/dnsrouter volumes: - - db_data:/var/lib/mysql + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro - cypress-mysql: + cypress: image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} build: - context: ../test/ - dockerfile: cypress/Dockerfile + context: ../ + dockerfile: test/cypress/Dockerfile environment: - CYPRESS_baseUrl: "http://fullstack-mysql:81" - volumes: - - cypress-logs:/results - command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} - - cypress-sqlite: - image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} - build: - context: ../test/ - dockerfile: cypress/Dockerfile - environment: - CYPRESS_baseUrl: "http://fullstack-sqlite:81" + CYPRESS_baseUrl: "http://fullstack:81" volumes: - cypress-logs:/results + - ./dev/resolv.conf:/etc/resolv.conf:ro command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} volumes: cypress-logs: - npm_data: - db_data: + npm_data_ci: + pdns_mysql_vol: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 79fbd799..44fe4232 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,9 +1,9 @@ -# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. -version: "3.5" +# WARNING: This is a DEVELOPMENT docker-compose file used for development of the entire app, it should not be used for production. +version: "3" services: + npm: image: nginxproxymanager:dev - container_name: npm_core build: context: ./ dockerfile: ./dev/Dockerfile @@ -11,52 +11,105 @@ services: - 3080:80 - 3081:81 - 3443:443 - networks: - - nginx_proxy_manager environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DEVELOPMENT: "true" - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" - # DB_SQLITE_FILE: "/data/database.sqlite" - # DISABLE_IPV6: "true" + DEVELOPMENT: 'true' + GOPROXY: "${GOPROXY:-}" + GOPRIVATE: "${GOPRIVATE:-}" + YARN_REGISTRY: "${DAB_YARN_REGISTRY:-}" + NPM_LOG_LEVEL: 'debug' + PUID: 1000 + PGID: 1000 volumes: - - npm_data:/data - - le_data:/etc/letsencrypt - - ../backend:/app - - ../frontend:/app/frontend - - ../global:/app/global - depends_on: - - db + - /etc/localtime:/etc/localtime:ro + - ../:/app + - ./rootfs/var/www/html:/var/www/html + - ../data:/data + - ./dev/resolv.conf:/etc/resolv.conf:ro working_dir: /app - - db: - image: jc21/mariadb-aria - container_name: npm_db - ports: - - 33306:3306 networks: - - nginx_proxy_manager + default: + aliases: + - website1.internal + - website2.internal + - website3.internal + + pebble: + image: letsencrypt/pebble + command: pebble -config /test/config/pebble-config.json environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" + PEBBLE_VA_SLEEPTIME: 2 volumes: - - db_data:/var/lib/mysql + - ./dev/pebble-config.json:/test/config/pebble-config.json + - ./dev/resolv.conf:/etc/resolv.conf:ro + networks: + default: + aliases: + # required for https cert dns san + - pebble + + stepca: + image: nginxproxymanager/testca + volumes: + - ./dev/resolv.conf:/etc/resolv.conf:ro + networks: + default: + aliases: + - ca.internal + + pdns: + image: pschiffe/pdns-mysql + volumes: + - '/etc/localtime:/etc/localtime:ro' + environment: + PDNS_master: 'yes' + PDNS_api: 'yes' + PDNS_api_key: 'npm' + PDNS_webserver: 'yes' + PDNS_webserver_address: '0.0.0.0' + PDNS_webserver_password: 'npm' + PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_version_string: 'anonymous' + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_gmysql_host: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + default: + aliases: + - ns1.pdns + - ns2.pdns + + pdns-db: + image: mariadb:10.7.1 + environment: + MYSQL_ROOT_PASSWORD: 'pdns' + MYSQL_DATABASE: 'pdns' + MYSQL_USER: 'pdns' + MYSQL_PASSWORD: 'pdns' + volumes: + - pdns_mysql_vol:/var/lib/mysql + - /etc/localtime:/etc/localtime:ro + - ./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro + + dnsrouter: + image: jc21/dnsrouter + volumes: + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + + swagger: + image: swaggerapi/swagger-ui:latest + ports: + - 3001:80 + environment: + URL: "http://${SWAGGER_PUBLIC_DOMAIN:-127.0.0.1:3081}/api/schema" + PORT: '80' + depends_on: + - npm volumes: - npm_data: - name: npm_core_data - le_data: - name: npm_le_data - db_data: - name: npm_db_data - -networks: - nginx_proxy_manager: - name: npm_network + pdns_mysql_vol: diff --git a/docker/rootfs/bin/check-health b/docker/rootfs/bin/check-health deleted file mode 100755 index bcf5552b..00000000 --- a/docker/rootfs/bin/check-health +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -OK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status') - -if [ "$OK" == "OK" ]; then - echo "OK" - exit 0 -else - echo "NOT OK" - exit 1 -fi diff --git a/docker/rootfs/bin/handle-ipv6-setting b/docker/rootfs/bin/handle-ipv6-setting deleted file mode 100755 index 2aa0e41a..00000000 --- a/docker/rootfs/bin/handle-ipv6-setting +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# This command reads the `DISABLE_IPV6` env var and will either enable -# or disable ipv6 in all nginx configs based on this setting. - -# Lowercase -DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]') - -CYAN='\E[1;36m' -BLUE='\E[1;34m' -YELLOW='\E[1;33m' -RED='\E[1;31m' -RESET='\E[0m' - -FOLDER=$1 -if [ "$FOLDER" == "" ]; then - echo -e "${RED}❯ $0 requires a absolute folder path as the first argument!${RESET}" - echo -e "${YELLOW} ie: $0 /data/nginx${RESET}" - exit 1 -fi - -FILES=$(find "$FOLDER" -type f -name "*.conf") -if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then - # IPV6 is disabled - echo "Disabling IPV6 in hosts" - echo -e "${BLUE}❯ ${CYAN}Disabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}" - - # Iterate over configs and run the regex - for FILE in $FILES - do - echo -e " ${BLUE}❯ ${YELLOW}${FILE}${RESET}" - sed -E -i 's/^([^#]*)listen \[::\]/\1#listen [::]/g' "$FILE" - done - -else - # IPV6 is enabled - echo -e "${BLUE}❯ ${CYAN}Enabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}" - - # Iterate over configs and run the regex - for FILE in $FILES - do - echo -e " ${BLUE}❯ ${YELLOW}${FILE}${RESET}" - sed -E -i 's/^(\s*)#listen \[::\]/\1listen [::]/g' "$FILE" - done - -fi diff --git a/docker/rootfs/bin/healthcheck.sh b/docker/rootfs/bin/healthcheck.sh new file mode 100755 index 00000000..63312086 --- /dev/null +++ b/docker/rootfs/bin/healthcheck.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euf -o pipefail + +HEALTHY="$(curl --silent "http://127.0.0.1:3000/api" | jq --raw-output '.result.healthy')" + +echo "Healthy: ${HEALTHY}" +[ "$HEALTHY" = 'true' ] || exit 1 diff --git a/docker/rootfs/etc/cont-init.d/.gitignore b/docker/rootfs/etc/cont-init.d/.gitignore deleted file mode 100644 index f04f0f6e..00000000 --- a/docker/rootfs/etc/cont-init.d/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!*.sh diff --git a/docker/rootfs/etc/cont-init.d/01_perms.sh b/docker/rootfs/etc/cont-init.d/01_perms.sh deleted file mode 100755 index e7875d32..00000000 --- a/docker/rootfs/etc/cont-init.d/01_perms.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/with-contenv bash -set -e - -mkdir -p /data/logs -echo "Changing ownership of /data/logs to $(id -u):$(id -g)" -chown -R "$(id -u):$(id -g)" /data/logs - diff --git a/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh b/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh deleted file mode 100644 index f145807a..00000000 --- a/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/with-contenv bash -# ref: https://github.com/linuxserver/docker-baseimage-alpine/blob/master/root/etc/cont-init.d/01-envfile - -# in s6, environmental variables are written as text files for s6 to monitor -# seach through full-path filenames for files ending in "__FILE" -for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do - echo "[secret-init] Evaluating ${FILENAME##*/} ..." - - # set SECRETFILE to the contents of the full-path textfile - SECRETFILE=$(cat ${FILENAME}) - # SECRETFILE=${FILENAME} - # echo "[secret-init] Set SECRETFILE to ${SECRETFILE}" # DEBUG - rm for prod! - - # if SECRETFILE exists / is not null - if [[ -f ${SECRETFILE} ]]; then - # strip the appended "__FILE" from environmental variable name ... - STRIPFILE=$(echo ${FILENAME} | sed "s/__FILE//g") - # echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod! - - # ... and set value to contents of secretfile - # since s6 uses text files, this is effectively "export ..." - printf $(cat ${SECRETFILE}) > ${STRIPFILE} - # echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!" - echo "[secret-init] Success! ${STRIPFILE##*/} set from ${FILENAME##*/}" - - else - echo "[secret-init] cannot find secret in ${FILENAME}" - fi -done diff --git a/docker/rootfs/etc/cont-init.d/10-nginx b/docker/rootfs/etc/cont-init.d/10-nginx new file mode 100755 index 00000000..b1e852c2 --- /dev/null +++ b/docker/rootfs/etc/cont-init.d/10-nginx @@ -0,0 +1,18 @@ +#!/usr/bin/with-contenv bash + +# Create required folders +mkdir -p /tmp/nginx/body \ + /run/nginx \ + /var/log/nginx \ + /var/lib/nginx/cache/public \ + /var/lib/nginx/cache/private \ + /var/cache/nginx/proxy_temp \ + /data/logs + +touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log && chmod -R 777 /var/cache/nginx + +# Dynamically generate resolvers file +echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf)" ";" > /etc/nginx/conf.d/include/resolvers.conf + +# Fire off acme.sh wrapper script to "install" itself if required +acme.sh -h > /dev/null 2>&1 diff --git a/docker/rootfs/etc/cont-init.d/20-adduser b/docker/rootfs/etc/cont-init.d/20-adduser new file mode 100755 index 00000000..31340800 --- /dev/null +++ b/docker/rootfs/etc/cont-init.d/20-adduser @@ -0,0 +1,33 @@ +#!/usr/bin/with-contenv bash + +PUID=${PUID:-911} +PGID=${PGID:-911} + +groupmod -g 1000 users || exit 1 +useradd -u "${PUID}" -U -d /data -s /bin/false npmuser || exit 1 +usermod -G users npmuser || exit 1 +groupmod -o -g "$PGID" npmuser || exit 1 + +echo "------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +User UID: $(id -u npmuser) +User GID: $(id -g npmuser) +------------------------------------- +" + +chown -R npmuser:npmuser /data +chown -R npmuser:npmuser /run/nginx +chown -R npmuser:npmuser /etc/nginx +chown -R npmuser:npmuser /tmp/nginx +chown -R npmuser:npmuser /var/cache/nginx +chown -R npmuser:npmuser /var/lib/nginx +chown -R npmuser:npmuser /var/log/nginx + +# Home for npmuser +mkdir -p /tmp/npmuserhome +chown -R npmuser:npmuser /tmp/npmuserhome diff --git a/docker/rootfs/etc/letsencrypt.ini b/docker/rootfs/etc/letsencrypt.ini deleted file mode 100644 index aae53b90..00000000 --- a/docker/rootfs/etc/letsencrypt.ini +++ /dev/null @@ -1,6 +0,0 @@ -text = True -non-interactive = True -webroot-path = /data/letsencrypt-acme-challenge -key-type = ecdsa -elliptic-curve = secp384r1 -preferred-chain = ISRG Root X1 diff --git a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager deleted file mode 100644 index 20c23ac6..00000000 --- a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager +++ /dev/null @@ -1,25 +0,0 @@ -/data/logs/*_access.log /data/logs/*/access.log { - create 0644 root root - weekly - rotate 4 - missingok - notifempty - compress - sharedscripts - postrotate - /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true - endscript -} - -/data/logs/*_error.log /data/logs/*/error.log { - create 0644 root root - weekly - rotate 10 - missingok - notifempty - compress - sharedscripts - postrotate - /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true - endscript -} \ No newline at end of file diff --git a/docker/rootfs/etc/nginx/conf.d/default.conf b/docker/rootfs/etc/nginx/conf.d/default.conf index 37d316db..0e360743 100644 --- a/docker/rootfs/etc/nginx/conf.d/default.conf +++ b/docker/rootfs/etc/nginx/conf.d/default.conf @@ -1,18 +1,10 @@ # "You are not configured" page, which is the default if another default doesn't exist server { - listen 80; - listen [::]:80; - - set $forward_scheme "http"; - set $server "127.0.0.1"; - set $port "80"; - - server_name localhost-nginx-proxy-manager; - access_log /data/logs/fallback_access.log standard; - error_log /data/logs/fallback_error.log warn; - include conf.d/include/assets.conf; + listen 80 default; + server_name localhost; + include conf.d/include/acme-challenge.conf; include conf.d/include/block-exploits.conf; - include conf.d/include/letsencrypt-acme-challenge.conf; + access_log /data/logs/default.log proxy; location / { index index.html; @@ -22,18 +14,13 @@ server { # First 443 Host, which is the default if another default doesn't exist server { - listen 443 ssl; - listen [::]:443 ssl; - - set $forward_scheme "https"; - set $server "127.0.0.1"; - set $port "443"; - + listen 443 ssl default; server_name localhost; - access_log /data/logs/fallback_access.log standard; - error_log /dev/null crit; - ssl_certificate /data/nginx/dummycert.pem; - ssl_certificate_key /data/nginx/dummykey.pem; + include conf.d/include/block-exploits.conf; + access_log /data/logs/default.log proxy; + + ssl_certificate /etc/ssl/certs/dummycert.pem; + ssl_certificate_key /etc/ssl/certs/dummykey.pem; include conf.d/include/ssl-ciphers.conf; return 444; diff --git a/docker/rootfs/etc/nginx/conf.d/dev.conf b/docker/rootfs/etc/nginx/conf.d/dev.conf index edbdec8a..ce8c1da7 100644 --- a/docker/rootfs/etc/nginx/conf.d/dev.conf +++ b/docker/rootfs/etc/nginx/conf.d/dev.conf @@ -1,10 +1,6 @@ server { listen 81 default; - listen [::]:81 default; - server_name nginxproxymanager-dev; - root /app/frontend/dist; - access_log /dev/null; location /api { return 302 /api/; @@ -16,14 +12,22 @@ server { proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; - proxy_pass http://127.0.0.1:3000/; + proxy_pass http://127.0.0.1:3000/api/; + } - proxy_read_timeout 15m; - proxy_send_timeout 15m; + location ~ .html { + try_files $uri =404; } location / { - index index.html; - try_files $uri $uri.html $uri/ /index.html; + add_header X-Served-By $host; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass http://127.0.0.1:9000; } } diff --git a/docker/rootfs/etc/nginx/conf.d/include/.gitignore b/docker/rootfs/etc/nginx/conf.d/include/.gitignore deleted file mode 100644 index 5291fe15..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/.gitignore +++ /dev/null @@ -1 +0,0 @@ -resolvers.conf diff --git a/docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf b/docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf new file mode 100644 index 00000000..db408c77 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf @@ -0,0 +1,17 @@ +# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) +# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel +# other regex checks, because in our other config files have regex rule that denies access to files with dotted names. +location ^~ /.well-known/acme-challenge/ { + auth_basic off; + auth_request off; + allow all; + default_type "text/plain"; + root "/data/.acme.sh/.well-known"; +} + +# Hide /acme-challenge subdirectory and return 404 on all requests. +# It is somewhat more secure than letting Nginx return 403. +# Ending slash is important! +location = /.well-known/acme-challenge/ { + return 404; +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf index e95c2e8b..7dd0f5ce 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/assets.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/assets.conf @@ -1,31 +1,31 @@ location ~* ^.*\.(css|js|jpe?g|gif|png|woff|eot|ttf|svg|ico|css\.map|js\.map)$ { - if_modified_since off; + if_modified_since off; - # use the public cache - proxy_cache public-cache; - proxy_cache_key $host$request_uri; + # use the public cache + proxy_cache public-cache; + proxy_cache_key $host$request_uri; - # ignore these headers for media - proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; + # ignore these headers for media + proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; - # cache 200s and also 404s (not ideal but there are a few 404 images for some reason) - proxy_cache_valid any 30m; - proxy_cache_valid 404 1m; + # cache 200s and also 404s (not ideal but there are a few 404 images for some reason) + proxy_cache_valid any 30m; + proxy_cache_valid 404 1m; - # strip this header to avoid If-Modified-Since requests - proxy_hide_header Last-Modified; - proxy_hide_header Cache-Control; - proxy_hide_header Vary; + # strip this header to avoid If-Modified-Since requests + proxy_hide_header Last-Modified; + proxy_hide_header Cache-Control; + proxy_hide_header Vary; - proxy_cache_bypass 0; - proxy_no_cache 0; + proxy_cache_bypass 0; + proxy_no_cache 0; - proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404; - proxy_connect_timeout 5s; - proxy_read_timeout 45s; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404; + proxy_connect_timeout 5s; + proxy_read_timeout 45s; - expires @30m; - access_log off; + expires @30m; + access_log off; - include conf.d/include/proxy.conf; + include conf.d/include/proxy.conf; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf index 093bda23..22360fc1 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf @@ -2,92 +2,92 @@ set $block_sql_injections 0; if ($query_string ~ "union.*select.*\(") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($query_string ~ "union.*all.*select.*") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($query_string ~ "concat.*\(") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($block_sql_injections = 1) { - return 403; + return 403; } ## Block file injections set $block_file_injections 0; if ($query_string ~ "[a-zA-Z0-9_]=http://") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($block_file_injections = 1) { - return 403; + return 403; } ## Block common exploits set $block_common_exploits 0; if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "proc/self/environ") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "base64_(en|de)code\(.*\)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($block_common_exploits = 1) { - return 403; + return 403; } ## Block spam set $block_spam 0; if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") { - set $block_spam 1; + set $block_spam 1; } if ($block_spam = 1) { - return 403; + return 403; } ## Block user agents @@ -95,42 +95,42 @@ set $block_user_agents 0; # Disable Akeeba Remote Control 2.5 and earlier if ($http_user_agent ~ "Indy Library") { - set $block_user_agents 1; + set $block_user_agents 1; } # Common bandwidth hoggers and hacking tools. if ($http_user_agent ~ "libwww-perl") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GetRight") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GetWeb!") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Go!Zilla") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Download Demon") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Go-Ahead-Got-It") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "TurnitinBot") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GrabNet") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($block_user_agents = 1) { - return 403; + return 403; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf index 15f0d285..5fd4810f 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf @@ -1,3 +1,3 @@ if ($scheme = "http") { - return 301 https://$host$request_uri; + return 301 https://$host$request_uri; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf b/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf deleted file mode 100644 index 34249325..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf +++ /dev/null @@ -1,2 +0,0 @@ -# This should be left blank is it is populated programatically -# by the application backend. diff --git a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf b/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf deleted file mode 100644 index ff2a7827..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf +++ /dev/null @@ -1,30 +0,0 @@ -# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) -# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel -# other regex checks, because in our other config files have regex rule that denies access to files with dotted names. -location ^~ /.well-known/acme-challenge/ { - # Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure - # we need to open up access by turning off auth and IP ACL for this location. - auth_basic off; - auth_request off; - allow all; - - # Set correct content type. According to this: - # https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29 - # Current specification requires "text/plain" or no content header at all. - # It seems that "text/plain" is a safe option. - default_type "text/plain"; - - # This directory must be the same as in /etc/letsencrypt/cli.ini - # as "webroot-path" parameter. Also don't forget to set "authenticator" parameter - # there to "webroot". - # Do NOT use alias, use root! Target directory is located here: - # /var/www/common/letsencrypt/.well-known/acme-challenge/ - root /data/letsencrypt-acme-challenge; -} - -# Hide /acme-challenge subdirectory and return 404 on all requests. -# It is somewhat more secure than letting Nginx return 403. -# Ending slash is important! -location = /.well-known/acme-challenge/ { - return 404; -} diff --git a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf index fcaaf003..b84a4513 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf @@ -3,6 +3,4 @@ proxy_set_header Host $host; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; -proxy_set_header X-Real-IP $remote_addr; -proxy_pass $forward_scheme://$server:$port$request_uri; - +proxy_pass $forward_scheme://$server:$port; diff --git a/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf b/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf new file mode 100644 index 00000000..ccd9dcef --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf @@ -0,0 +1 @@ +# Intentionally blank diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf index 233abb6e..bd905d31 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf @@ -3,5 +3,7 @@ ssl_session_cache shared:SSL:50m; # intermediate configuration. tweak to your needs. ssl_protocols TLSv1.2 TLSv1.3; -ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; -ssl_prefer_server_ciphers off; +ssl_ciphers 'EECDH+AESGCM:AES256+EECDH:AES256+EDH:EDH+AESGCM:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE- +ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE +S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES'; +ssl_prefer_server_ciphers on; diff --git a/docker/rootfs/etc/nginx/conf.d/production.conf b/docker/rootfs/etc/nginx/conf.d/production.conf index 877e51dd..325cb8cc 100644 --- a/docker/rootfs/etc/nginx/conf.d/production.conf +++ b/docker/rootfs/etc/nginx/conf.d/production.conf @@ -1,33 +1,14 @@ # Admin Interface server { listen 81 default; - listen [::]:81 default; - server_name nginxproxymanager; - root /app/frontend; - access_log /dev/null; - location /api { - return 302 /api/; - } - - location /api/ { + location / { add_header X-Served-By $host; proxy_set_header Host $host; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; - proxy_pass http://127.0.0.1:3000/; - - proxy_read_timeout 15m; - proxy_send_timeout 15m; - } - - location / { - index index.html; - if ($request_uri ~ ^/(.*)\.html$) { - return 302 /$1; - } - try_files $uri $uri.html $uri/ /index.html; + proxy_pass http://localhost:3000/; } } diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 4d5ee901..f815c1b7 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -1,7 +1,7 @@ # run nginx in foreground daemon off; - -user root; +user npmuser; +pid /run/nginx/nginx.pid; # Set number of worker processes automatically based on number of CPU cores. worker_processes auto; @@ -9,7 +9,7 @@ worker_processes auto; # Enables the use of JIT for regular expressions to speed-up their processing. pcre_jit on; -error_log /data/logs/fallback_error.log warn; +error_log /data/logs/error.log warn; # Includes files with directives to load dynamic modules. include /etc/nginx/modules/*.conf; @@ -26,15 +26,12 @@ http { tcp_nopush on; tcp_nodelay on; client_body_temp_path /tmp/nginx/body 1 2; - keepalive_timeout 90s; - proxy_connect_timeout 90s; - proxy_send_timeout 90s; - proxy_read_timeout 90s; + keepalive_timeout 65; ssl_prefer_server_ciphers on; gzip on; proxy_ignore_client_abort off; - client_max_body_size 2000m; - server_names_hash_bucket_size 1024; + client_max_body_size 200m; + server_names_hash_bucket_size 64; proxy_http_version 1.1; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -43,13 +40,8 @@ http { proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; - log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; - log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; - - access_log /data/logs/fallback_access.log proxy; - - # Dynamically generated resolvers file - include /etc/nginx/conf.d/include/resolvers.conf; + log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; + access_log /data/logs/default.log proxy; # Default upstream scheme map $host $forward_scheme { @@ -57,39 +49,18 @@ http { } # Real IP Determination - - # Local subnets: - set_real_ip_from 10.0.0.0/8; - set_real_ip_from 172.16.0.0/12; # Includes Docker subnet - set_real_ip_from 192.168.0.0/16; + # Docker subnet: + set_real_ip_from 172.0.0.0/8; # NPM generated CDN ip ranges: - include conf.d/include/ip_ranges.conf; + #include conf.d/include/ip_ranges.conf; # always put the following 2 lines after ip subnets: - real_ip_header X-Real-IP; + real_ip_header X-Forwarded-For; real_ip_recursive on; - # Custom - include /data/nginx/custom/http_top[.]conf; - # Files generated by NPM include /etc/nginx/conf.d/*.conf; include /data/nginx/default_host/*.conf; include /data/nginx/proxy_host/*.conf; include /data/nginx/redirection_host/*.conf; include /data/nginx/dead_host/*.conf; - include /data/nginx/temp/*.conf; - - # Custom - include /data/nginx/custom/http[.]conf; } - -stream { - # Files generated by NPM - include /data/nginx/stream/*.conf; - - # Custom - include /data/nginx/custom/stream[.]conf; -} - -# Custom -include /data/nginx/custom/root[.]conf; diff --git a/docker/rootfs/etc/services.d/backend/finish b/docker/rootfs/etc/services.d/backend/finish new file mode 100755 index 00000000..2b661f61 --- /dev/null +++ b/docker/rootfs/etc/services.d/backend/finish @@ -0,0 +1,5 @@ +#!/usr/bin/execlineb -S1 +if { s6-test ${1} -ne 0 } +if { s6-test ${1} -ne 256 } + +s6-svscanctl -t /var/run/s6/services diff --git a/docker/rootfs/etc/services.d/backend/run b/docker/rootfs/etc/services.d/backend/run new file mode 100755 index 00000000..b63ad826 --- /dev/null +++ b/docker/rootfs/etc/services.d/backend/run @@ -0,0 +1,23 @@ +#!/usr/bin/with-contenv bash + +RESET='\E[0m' +YELLOW='\E[1;33m' + +echo -e "${YELLOW}Starting backend API ...${RESET}" + +if [ "$DEVELOPMENT" == "true" ]; then + HOME=/tmp/npmuserhome + GOPATH="$HOME/go" + mkdir -p "$GOPATH" + chown -R npmuser:npmuser "$GOPATH" + export HOME GOPATH + cd /app/backend || exit 1 + s6-setuidgid npmuser task -w +else + cd /app/bin || exit 1 + while : + do + s6-setuidgid npmuser /app/bin/server + sleep 1 + done +fi diff --git a/docker/rootfs/etc/services.d/frontend/finish b/docker/rootfs/etc/services.d/frontend/finish index bca9a35d..2b661f61 100755 --- a/docker/rootfs/etc/services.d/frontend/finish +++ b/docker/rootfs/etc/services.d/frontend/finish @@ -3,4 +3,3 @@ if { s6-test ${1} -ne 0 } if { s6-test ${1} -ne 256 } s6-svscanctl -t /var/run/s6/services - diff --git a/docker/rootfs/etc/services.d/frontend/run b/docker/rootfs/etc/services.d/frontend/run index a666d53e..87963721 100755 --- a/docker/rootfs/etc/services.d/frontend/run +++ b/docker/rootfs/etc/services.d/frontend/run @@ -3,10 +3,13 @@ # This service is DEVELOPMENT only. if [ "$DEVELOPMENT" == "true" ]; then + CI=true + HOME=/tmp/npmuserhome + export CI + export HOME cd /app/frontend || exit 1 - # If yarn install fails: add --verbose --network-concurrency 1 - yarn install - yarn watch + s6-setuidgid npmuser yarn install + s6-setuidgid npmuser yarn start else exit 0 fi diff --git a/docker/rootfs/etc/services.d/manager/finish b/docker/rootfs/etc/services.d/manager/finish deleted file mode 100755 index 7d442d6a..00000000 --- a/docker/rootfs/etc/services.d/manager/finish +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/with-contenv bash - -s6-svscanctl -t /var/run/s6/services diff --git a/docker/rootfs/etc/services.d/manager/run b/docker/rootfs/etc/services.d/manager/run deleted file mode 100755 index e365f4fb..00000000 --- a/docker/rootfs/etc/services.d/manager/run +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/with-contenv bash - -mkdir -p /data/letsencrypt-acme-challenge - -cd /app || echo - -if [ "$DEVELOPMENT" == "true" ]; then - cd /app || exit 1 - # If yarn install fails: add --verbose --network-concurrency 1 - yarn install - node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js -else - cd /app || exit 1 - while : - do - node --abort_on_uncaught_exception --max_old_space_size=250 index.js - sleep 1 - done -fi diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/rootfs/etc/services.d/nginx/finish deleted file mode 120000 index 63b10de4..00000000 --- a/docker/rootfs/etc/services.d/nginx/finish +++ /dev/null @@ -1 +0,0 @@ -/bin/true \ No newline at end of file diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/rootfs/etc/services.d/nginx/finish new file mode 100755 index 00000000..bca9a35d --- /dev/null +++ b/docker/rootfs/etc/services.d/nginx/finish @@ -0,0 +1,6 @@ +#!/usr/bin/execlineb -S1 +if { s6-test ${1} -ne 0 } +if { s6-test ${1} -ne 256 } + +s6-svscanctl -t /var/run/s6/services + diff --git a/docker/rootfs/etc/services.d/nginx/run b/docker/rootfs/etc/services.d/nginx/run index 51ca5ea1..e04643e7 100755 --- a/docker/rootfs/etc/services.d/nginx/run +++ b/docker/rootfs/etc/services.d/nginx/run @@ -1,49 +1,3 @@ #!/usr/bin/with-contenv bash -# Create required folders -mkdir -p /tmp/nginx/body \ - /run/nginx \ - /var/log/nginx \ - /data/nginx \ - /data/custom_ssl \ - /data/logs \ - /data/access \ - /data/nginx/default_host \ - /data/nginx/default_www \ - /data/nginx/proxy_host \ - /data/nginx/redirection_host \ - /data/nginx/stream \ - /data/nginx/dead_host \ - /data/nginx/temp \ - /var/lib/nginx/cache/public \ - /var/lib/nginx/cache/private \ - /var/cache/nginx/proxy_temp - -touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log && chmod -R 777 /var/cache/nginx -chown root /tmp/nginx - -# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` -# thanks @tfmm -echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf - -# Generate dummy self-signed certificate. -if [ ! -f /data/nginx/dummycert.pem ] || [ ! -f /data/nginx/dummykey.pem ] -then - echo "Generating dummy SSL certificate..." - openssl req \ - -new \ - -newkey rsa:2048 \ - -days 3650 \ - -nodes \ - -x509 \ - -subj '/O=localhost/OU=localhost/CN=localhost' \ - -keyout /data/nginx/dummykey.pem \ - -out /data/nginx/dummycert.pem - echo "Complete" -fi - -# Handle IPV6 settings -/bin/handle-ipv6-setting /etc/nginx/conf.d -/bin/handle-ipv6-setting /data/nginx - -exec nginx +exec s6-setuidgid npmuser nginx diff --git a/docker/rootfs/root/.bashrc b/docker/rootfs/root/.bashrc index 1deb975c..122da279 100644 --- a/docker/rootfs/root/.bashrc +++ b/docker/rootfs/root/.bashrc @@ -16,7 +16,5 @@ alias h='cd ~;clear;' echo -e -n '\E[1;34m' figlet -w 120 "NginxProxyManager" -echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, ${ID:-debian} \E[1;32m${VERSION:-unknown}\E[1;36m, Certbot \E[1;32m$(certbot --version)\E[0m" -echo -e -n '\E[1;34m' -cat /built-for-arch -echo -e '\E[0m' +echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-3.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, Debian \E[1;32m${VERSION_ID:-unknown}\E[1;36m, Kernel \E[1;32m$(uname -r)\E[0m" +echo diff --git a/docker/rootfs/root/.config/litecli/config b/docker/rootfs/root/.config/litecli/config new file mode 100644 index 00000000..bfc67aa8 --- /dev/null +++ b/docker/rootfs/root/.config/litecli/config @@ -0,0 +1,13 @@ +[main] +multi_line = True +log_level = INFO +table_format = psql +syntax_style = monokai +wider_completion_menu = True +prompt = '\d> ' +prompt_continuation = '-> ' +less_chatty = True +auto_vertical_output = True + +[favorite_queries] +show_users = select * from user diff --git a/docker/rootfs/var/www/html/index.html b/docker/rootfs/var/www/html/index.html index 8478b47f..2a437c36 100644 --- a/docker/rootfs/var/www/html/index.html +++ b/docker/rootfs/var/www/html/index.html @@ -1,24 +1,24 @@ - - - - - Default Site - - - - -
-
-

Congratulations!

-

You've successfully started the Nginx Proxy Manager.

-

If you're seeing this site then you're trying to access a host that isn't set up yet.

-

Log in to the Admin panel to get started.

-
-

Powered by Nginx Proxy Manager

-
- + + + + + Default Site + + + + +
+
+

Congratulations!

+

You've successfully started the Nginx Proxy Manager.

+

If you're seeing this site then you're trying to access a host that isn't set up yet.

+

Log in to the Admin panel to get started.

+
+

Powered by Nginx Proxy Manager

+
+ diff --git a/docs/.gitignore b/docs/.gitignore index 38353fb5..eccfbb30 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,5 @@ .vuepress/dist node_modules ts +api.md +api/ diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index f3b735b8..562ed723 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -39,7 +39,7 @@ module.exports = { // Custom text for edit link. Defaults to "Edit this page" editLinkText: "Edit this page on GitHub", // Custom navbar values - nav: [{ text: "Setup", link: "/setup/" }], + nav: [{ text: "Setup", link: "/setup/" }, { text: "API", link: "/api/index.html" }], // Custom sidebar values sidebar: [ "/", diff --git a/docs/README.md b/docs/README.md index 082bb05c..4356775b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ home: true heroImage: /logo.png actionText: Get Started → actionLink: /guide/ -footer: MIT Licensed | Copyright © 2016-present jc21.com +footer: MIT Licensed | Copyright © 2016-2021 jc21.com ---
@@ -37,3 +37,37 @@ footer: MIT Licensed | Copyright © 2016-present jc21.com

Configure other users to either view or manage their own hosts. Full access permissions are available.

+ +### Quick Setup + +1. Install Docker and Docker-Compose + +- [Docker Install documentation](https://docs.docker.com/install/) +- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) + +2. Create a docker-compose.yml file similar to this: + +```yml +version: '3' +services: + app: + image: 'jc21/nginx-proxy-manager:3' + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data +``` + +3. Bring up your stack + +```bash +docker-compose up -d +``` + +4. Log in to the Admin UI + +When your docker container is running, connect to it on port `81` for the admin interface. + +[http://127.0.0.1:81](http://127.0.0.1:81) diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index c7b51a84..61820795 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -1,10 +1,10 @@ # Advanced Configuration -## Best Practice: Use a Docker network +## Best Practice: Use a docker network -For those who have a few of their upstream services running in Docker on the same Docker -host as NPM, here's a trick to secure things a bit better. By creating a custom Docker network, -you don't need to publish ports for your upstream services to all of the Docker host's interfaces. +For those who have a few of their upstream services running in docker on the same docker +host as NPM, here's a trick to secure things a bit better. By creating a custom docker network, +you don't need to publish ports for your upstream services to all of the docker host's interfaces. Create a network, ie "scoobydoo": @@ -13,7 +13,7 @@ docker network create scoobydoo ``` Then add the following to the `docker-compose.yml` file for both NPM and any other -services running on this Docker host: +services running on this docker host: ```yml networks: @@ -44,22 +44,10 @@ networks: Now in the NPM UI you can create a proxy host with `portainer` as the hostname, and port `9000` as the port. Even though this port isn't listed in the docker-compose -file, it's "exposed" by the Portainer Docker image for you and not available on -the Docker host outside of this Docker network. The service name is used as the +file, it's "exposed" by the portainer docker image for you and not available on +the docker host outside of this docker network. The service name is used as the hostname, so make sure your service names are unique when using the same network. -## Docker Healthcheck - -The `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this -feature by adding the following to the service in your `docker-compose.yml` file: - -```yml -healthcheck: - test: ["CMD", "/bin/check-health"] - interval: 10s - timeout: 3s -``` - ## Docker Secrets This image supports the use of Docker secrets to import from file and keep sensitive usernames or passwords from being passed or preserved in plaintext. @@ -128,7 +116,7 @@ services: ## Disabling IPv6 -On some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log: +On some docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log: > Address family not supported by protocol diff --git a/docs/faq/README.md b/docs/faq/README.md index cf739ead..1703e705 100644 --- a/docs/faq/README.md +++ b/docs/faq/README.md @@ -21,6 +21,3 @@ Your best bet is to ask the [Reddit community for support](https://www.reddit.co Gitter is best left for anyone contributing to the project to ask for help about internals, code reviews etc. -## When adding username and password access control to a proxy host, I can no longer login into the app. - -Having an Access Control List (ACL) with username and password requires the browser to always send this username and password in the `Authorization` header on each request. If your proxied app also requires authentication (like Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information, as this is the standardized header meant for this kind of information. However having multiples of the same headers is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization. \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index dc28e5a0..a1cec675 100644 --- a/docs/package.json +++ b/docs/package.json @@ -357,7 +357,7 @@ "jsbn": "^1.1.0", "jsesc": "^3.0.1", "json-parse-better-errors": "^1.0.2", - "json-schema": "^0.4.0", + "json-schema": "^0.2.5", "json-schema-traverse": "^0.4.1", "json-stringify-safe": "^5.0.1", "json3": "^3.3.3", @@ -394,7 +394,7 @@ "map-age-cleaner": "^0.1.3", "map-cache": "^0.2.2", "map-visit": "^1.0.0", - "markdown-it": "^12.3.2", + "markdown-it": "^11.0.0", "markdown-it-anchor": "^5.3.0", "markdown-it-chain": "^1.3.0", "markdown-it-container": "^3.0.0", @@ -434,7 +434,7 @@ "neo-async": "^2.6.2", "nice-try": "^2.0.1", "no-case": "^3.0.3", - "node-forge": "^1.0.0", + "node-forge": "^0.10.0", "node-libs-browser": "^2.2.1", "node-releases": "^1.1.60", "nopt": "^4.0.3", @@ -443,7 +443,7 @@ "normalize-url": "^5.1.0", "npm-run-path": "^4.0.1", "nprogress": "^0.2.0", - "nth-check": "^2.0.1", + "nth-check": "^1.0.2", "num2fraction": "^1.2.2", "number-is-nan": "^2.0.0", "oauth-sign": "^0.9.0", @@ -612,7 +612,7 @@ "serve-index": "^1.9.1", "serve-static": "^1.14.1", "set-blocking": "^2.0.0", - "set-value": "^4.0.1", + "set-value": "^3.0.2", "setimmediate": "^1.0.5", "setprototypeof": "^1.2.0", "sha.js": "^2.4.11", diff --git a/docs/setup/README.md b/docs/setup/README.md index b9c42274..a2ff7392 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -1,35 +1,31 @@ # Full Setup Instructions -## Running the App +### Running the App -Create a `docker-compose.yml` file: +Via `docker-compose`: ```yml version: "3" services: app: - image: 'jc21/nginx-proxy-manager:latest' - restart: unless-stopped + image: 'jc21/nginx-proxy-manager:v3-develop' + restart: always ports: - # These ports are in format : - - '80:80' # Public HTTP Port - - '443:443' # Public HTTPS Port - - '81:81' # Admin Web Port - # Add any other Stream port you want to expose - # - '21:21' # FTP - - # Uncomment the next line if you uncomment anything in the section - # environment: - # Uncomment this if you want to change the location of - # the SQLite DB file within the container - # DB_SQLITE_FILE: "/data/database.sqlite" - + # Public HTTP Port: + - '80:80' + # Public HTTPS Port: + - '443:443' + # Admin Web Port: + - '81:81' + environment: + # These run the processes and own the files + # for a specific user/group + - PUID=1000 + - PGID=1000 # Uncomment this if IPv6 is not enabled on your host # DISABLE_IPV6: 'true' - volumes: - ./data:/data - - ./letsencrypt:/etc/letsencrypt ``` Then: @@ -38,64 +34,7 @@ Then: docker-compose up -d ``` -## Using MySQL / MariaDB Database - -If you opt for the MySQL configuration you will have to provide the database server yourself. You can also use MariaDB. Here are the minimum supported versions: - -- MySQL v5.7.8+ -- MariaDB v10.2.7+ - -It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples -are going to use. - -Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container: - -```yml -version: "3" -services: - app: - image: 'jc21/nginx-proxy-manager:latest' - restart: unless-stopped - ports: - # These ports are in format : - - '80:80' # Public HTTP Port - - '443:443' # Public HTTPS Port - - '81:81' # Admin Web Port - # Add any other Stream port you want to expose - # - '21:21' # FTP - environment: - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" - # Uncomment this if IPv6 is not enabled on your host - # DISABLE_IPV6: 'true' - volumes: - - ./data:/data - - ./letsencrypt:/etc/letsencrypt - depends_on: - - db - - db: - image: 'jc21/mariadb-aria:latest' - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: 'npm' - MYSQL_DATABASE: 'npm' - MYSQL_USER: 'npm' - MYSQL_PASSWORD: 'npm' - volumes: - - ./data/mysql:/var/lib/mysql -``` - -::: warning - -Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use SQLite. - -::: - -## Running on Raspberry PI / ARM devices +### Running on Raspberry PI / ARM devices The docker images support the following architectures: - amd64 @@ -107,76 +46,17 @@ you don't have to worry about doing anything special and you can follow the comm Check out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) for a list of supported architectures and if you want one that doesn't exist, -[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). +[create a feature request](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) on Raspbian. -Please note that the `jc21/mariadb-aria:latest` image might have some problems on some ARM devices, if you want a separate database container, use the `yobasystems/alpine-mariadb:latest` image. -## Initial Run +### Initial Run After the app is running for the first time, the following will happen: 1. The database will initialize with table structures 2. GPG keys will be generated and saved in the configuration file -3. A default admin user will be created This process can take a couple of minutes depending on your machine. - - -## Default Administrator User - -``` -Email: admin@example.com -Password: changeme -``` - -Immediately after logging in with this default user you will be asked to modify your details and change your password. - -## Configuration File - -::: warning - -This section is meant for advanced users - -::: - -If you would like more control over the database settings you can define a custom config JSON file. - - -Here's an example for `sqlite` configuration as it is generated from the environment variables: - -```json -{ - "database": { - "engine": "knex-native", - "knex": { - "client": "sqlite3", - "connection": { - "filename": "/data/database.sqlite" - }, - "useNullAsDefault": true - } - } -} -``` - -You can modify the `knex` object with your custom configuration, but note that not all knex clients might be installed in the image. - -Once you've created your configuration file you can mount it to `/app/config/production.json` inside you container using: - -``` -[...] -services: - app: - image: 'jc21/nginx-proxy-manager:latest' - [...] - volumes: - - ./config.json:/app/config/production.json - [...] -[...] -``` - -**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. -These keys affect the login and session management of the application. If these keys change for any reason, all users will be logged out. diff --git a/docs/yarn.lock b/docs/yarn.lock index 843a2415..5f981d38 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -1624,9 +1624,9 @@ ansi-regex@^4.1.0: integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-regex@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== ansi-styles@^2.2.1: version "2.2.1" @@ -1686,11 +1686,6 @@ argparse@^1.0.10, argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -2565,7 +2560,7 @@ cli-boxes@^2.2.0: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== -clipboard@^2.0.6: +clipboard@^2.0.0, clipboard@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== @@ -2655,9 +2650,9 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^1.5.2, color-string@^1.5.3: - version "1.5.5" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" - integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -3749,10 +3744,10 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0, entities@^2.0.3, entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +entities@^2.0.0, entities@^2.0.3, entities@~2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== envify@^4.0.0, envify@^4.1.0: version "4.1.0" @@ -4117,13 +4112,6 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-xml-parser@^3.19.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736" - integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg== - dependencies: - strnum "^1.0.4" - fastq@^1.6.0: version "1.8.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" @@ -4266,9 +4254,9 @@ flush-write-stream@^2.0.0: readable-stream "^3.1.1" follow-redirects@^1.0.0, follow-redirects@^1.12.1: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + version "1.12.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" + integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== for-in@^1.0.2: version "1.0.2" @@ -5542,11 +5530,11 @@ is-svg@^3.0.0: html-comment-regex "^1.1.0" is-svg@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.0.tgz#3e46a45dcdb2780e42a3c8538154d7f7bfc07216" - integrity sha512-Np3TOGLVr0J27VDaS/gVE7bT45ZcSmX4pMmMTsPjqO8JY383fuPIcWmZr3QsHVWhqhZWxSdmW+tkkl3PWOB0Nw== + version "4.2.2" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.2.2.tgz#a4ea0f3f78dada7085db88f1e85b6f845626cfae" + integrity sha512-JlA7Mc7mfWjdxxTkJ094oUK9amGD7gQaj5xA/NCY0vlVvZ1stmj4VX+bRuwOMN93IHRZ2ctpPH/0FO6DqvQ5Rw== dependencies: - fast-xml-parser "^3.19.0" + html-comment-regex "^1.1.2" is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" @@ -5726,10 +5714,10 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-schema@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== +json-schema@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" + integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ== json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" @@ -6147,13 +6135,13 @@ markdown-it-table-of-contents@^0.4.0, markdown-it-table-of-contents@^0.4.4: resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc" integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw== -markdown-it@^12.3.2: - version "12.3.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" - integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== +markdown-it@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.0.tgz#dbfc30363e43d756ebc52c38586b91b90046b876" + integrity sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg== dependencies: - argparse "^2.0.1" - entities "~2.1.0" + argparse "^1.0.7" + entities "~2.0.0" linkify-it "^3.0.1" mdurl "^1.0.1" uc.micro "^1.0.5" @@ -6417,10 +6405,10 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== +minizlib@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3" + integrity sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA== dependencies: minipass "^3.0.0" yallist "^4.0.0" @@ -6607,10 +6595,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-forge@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.0.0.tgz#a025e3beeeb90d9cee37dae34d25b968ec3e6f15" - integrity sha512-ShkiiAlzSsgH1IwGlA0jybk9vQTIOLyJ9nBd0JTuP+nzADJFLY0NoDijM2zvD/JaezooGu3G2p2FNxOAK6459g== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-libs-browser@^2.2.1: version "2.2.1" @@ -6738,13 +6726,6 @@ nth-check@^1.0.2, nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -nth-check@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" - integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== - dependencies: - boolbase "^1.0.0" - num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -7192,9 +7173,9 @@ path-key@^3.0.0, path-key@^3.1.0, path-key@^3.1.1: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== path-to-regexp@0.1.7: version "0.1.7" @@ -7718,9 +7699,11 @@ pretty-time@^1.1.0: integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== prismjs@^1.13.0, prismjs@^1.20.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" - integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== + version "1.23.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" + integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== + optionalDependencies: + clipboard "^2.0.0" private@^0.1.8: version "0.1.8" @@ -8439,9 +8422,9 @@ set-blocking@^2.0.0: integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= set-getter@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102" - integrity sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw== + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= dependencies: to-object-path "^0.3.0" @@ -8455,20 +8438,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -set-value@^3.0.0: +set-value@^3.0.0, set-value@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== dependencies: is-plain-object "^2.0.4" -set-value@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.0.1.tgz#bc23522ade2d52314ec3b5d6fb140f5cd3a88acf" - integrity sha512-ayATicCYPVnlNpFmjq2/VmVwhoCQA9+13j8qWp044fmFE3IFphosPtRM+0CJ5xoIx5Uy52fCcwg3XeH2pHbbPQ== - dependencies: - is-plain-object "^2.0.4" - setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -9092,11 +9068,6 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strnum@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" - integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== - stylehacks@^4.0.0, stylehacks@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -9185,14 +9156,14 @@ tapable@^1.0.0, tapable@^1.1.3: integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar@^6.0.2: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + version "6.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" + integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" minipass "^3.0.0" - minizlib "^2.1.1" + minizlib "^2.1.0" mkdirp "^1.0.3" yallist "^4.0.0" @@ -9681,9 +9652,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.4.7: - version "1.5.9" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.9.tgz#05ff26484a0b5e4040ac64dcee4177223d74675e" - integrity sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ== + version "1.5.0" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.0.tgz#90aba6c902aeb2d80eac17b91131c27665d5d828" + integrity sha512-9iT6N4s93SMfzunOyDPe4vo4nLcSu1yq0IQK1gURmjm8tQNlM6loiuCRrKG1hHGXfB2EWd6H4cGi7tGdaygMFw== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" diff --git a/frontend/.babelrc b/frontend/.babelrc deleted file mode 100644 index 54071ecd..00000000 --- a/frontend/.babelrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "presets": [ - [ - "env", - { - "targets": { - "browsers": [ - "Chrome >= 65" - ] - }, - "debug": false, - "modules": false, - "useBuiltIns": "usage" - } - ] - ] -} \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 00000000..6d0e75ca --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,4 @@ +PORT=9000 +IMAGE_INLINE_SIZE_LIMIT=20000 +REACT_APP_VERSION=development +REACT_APP_COMMIT=local \ No newline at end of file diff --git a/frontend/.eslintrc b/frontend/.eslintrc new file mode 100644 index 00000000..383f205c --- /dev/null +++ b/frontend/.eslintrc @@ -0,0 +1,127 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "prettier", + "import", + "react-hooks" + ], + "extends": [ + "react-app", + "eslint-config-prettier", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier" + ], + "env": { + "jest": true, + "browser": true, + "commonjs": true + }, + "rules": { + "prettier/prettier": [ + "error" + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } + ], + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": [ + "off" + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "off" + ], + "@typescript-eslint/explicit-member-accessibility": [ + "off" + ], + "@typescript-eslint/no-empty-function": [ + "off" + ], + "@typescript-eslint/no-explicit-any": [ + "off" + ], + "@typescript-eslint/no-non-null-assertion": [ + "off" + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": [ + "camelCase", + "PascalCase", + "UPPER_CASE" + ], + "leadingUnderscore": "allow", + "trailingUnderscore": "allow" + } + ], + "react-hooks/rules-of-hooks": [ + "error" + ], + "react-hooks/exhaustive-deps": [ + "warn", + { + "additionalHooks": "useAction|useReduxAction" + } + ], + "react/jsx-curly-brace-presence": [ + "warn", + { + "props": "never", + "children": "never", + } + ], + "no-restricted-globals": [ + "off" + ], + "import/extensions": 0, // We let webpack handle resolving file extensions + "import/order": [ + "error", + { + "alphabetize": { + "order": "asc", + "caseInsensitive": true + }, + "newlines-between": "always", + "pathGroups": [ + { + "pattern": "@(react)", + "group": "external", + "position": "before" + }, + { + "pattern": "@/@(fixtures|jest)/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/**", + "group": "internal" + } + ], + "pathGroupsExcludedImportTypes": [ + "builtin", + "internal" + ], + "groups": [ + "builtin", + "external", + "internal", + [ + "parent", + "sibling", + "index" + ] + ] + } + ] + } +} \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index c8f4b4f9..aa97ba4b 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,4 +1,4 @@ -dist -node_modules -webpack_stats.html -yarn-error.log +.eslintcache +coverage +junit.xml +eslint.xml diff --git a/backend/.prettierrc b/frontend/.prettierrc similarity index 55% rename from backend/.prettierrc rename to frontend/.prettierrc index fefbcfa6..9e6bec4a 100644 --- a/backend/.prettierrc +++ b/frontend/.prettierrc @@ -1,11 +1,11 @@ { - "printWidth": 320, - "tabWidth": 4, + "printWidth": 80, + "tabWidth": 2, "useTabs": true, "semi": true, - "singleQuote": true, + "singleQuote": false, "bracketSpacing": true, - "jsxBracketSameLine": true, + "bracketSameLine": true, "trailingComma": "all", "proseWrap": "always" } diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..c0b8c7ec --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,20 @@ +# NPM Frontend + +The frontend package is a React based UI, bootstrapped with +[Create React App](https://github.com/facebook/create-react-app) and written in +Typescript. + +## React Modules + +- [Chakra UI](https://chakra-ui.com/) +- [Formik](https://formik.org/) +- [React Query](https://react-query.tanstack.com/) +- [React Table](https://react-table.tanstack.com/) +- [React Router](https://reactrouter.com/) +- [Format.js](https://formatjs.io/docs/getting-started/installation/) +- [Rooks](https://react-hooks.org/) + +Some light reading that inspired the concepts used in the UI: + +- [Using react-query with react-table](https://nafeu.medium.com/using-react-query-with-react-table-884158535424) +- [Server side pagination using react-table and react-query](https://dev.to/elangobharathi/server-side-pagination-using-react-table-v7-and-react-query-v3-3lck) diff --git a/frontend/check-locales.js b/frontend/check-locales.js new file mode 100755 index 00000000..0e71ab40 --- /dev/null +++ b/frontend/check-locales.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +// This file does a few things to ensure that the Locales are present and valid: +// - Ensures that the name of the locale exists in the language list +// - Ensures that each locale contains the translations used in the application +// - Ensures that there are no unused translations in the locale files +// - Also checks the error messages returned by the backend + +const allLocales = [ + ["en", "en-US"], + ["de", "de-DE"], + ["fa", "fa-IR"], +]; + +const ignoreUnused = [/^capability\..*$/, /^host-type\..*$/, /^acmesh\..*$/]; + +const { spawnSync } = require("child_process"); +const fs = require("fs"); + +const tmp = require("tmp"); + +// Parse backend errors +const BACKEND_ERRORS_FILE = "../backend/internal/errors/errors.go"; +const BACKEND_ERRORS = []; +try { + const backendErrorsContent = fs.readFileSync(BACKEND_ERRORS_FILE, "utf8"); + const backendErrorsContentRes = [ + ...backendErrorsContent.matchAll(/errors\.New\("([^"]+)"\)/g), + ]; + backendErrorsContentRes.map((item) => { + BACKEND_ERRORS.push("error." + item[1]); + return null; + }); +} catch (err) { + console.log("\x1b[31m%s\x1b[0m", err); + process.exit(1); +} + +// get all translations used in frontend code +const tmpobj = tmp.fileSync({ postfix: ".json" }); +spawnSync("yarn", ["locale-extract", "--out-file", tmpobj.name]); + +const allLocalesInProject = require(tmpobj.name); + +// get list og language names and locales +const langList = require("./src/locale/src/lang-list.json"); + +// store a list of all validation errors +const allErrors = []; + +const checkLangList = (fullCode) => { + const key = "locale-" + fullCode; + if (typeof langList[key] === "undefined") { + allErrors.push( + "ERROR: `" + key + "` language does not exist in lang-list.json", + ); + } +}; + +const compareLocale = (locale) => { + const projectLocaleKeys = Object.keys(allLocalesInProject); + // Check that locale contains the items used in the codebase + projectLocaleKeys.map((key) => { + if (typeof locale.data[key] === "undefined") { + allErrors.push( + "ERROR: `" + locale[0] + "` does not contain item: `" + key + "`", + ); + } + return null; + }); + // Check that locale contains all error.* items + BACKEND_ERRORS.forEach((key) => { + if (typeof locale.data[key] === "undefined") { + allErrors.push( + "ERROR: `" + locale[0] + "` does not contain item: `" + key + "`", + ); + } + return null; + }); + + // Check that locale does not contain items not used in the codebase + const localeKeys = Object.keys(locale.data); + localeKeys.map((key) => { + let ignored = false; + ignoreUnused.map((regex) => { + if (key.match(regex)) { + ignored = true; + } + return null; + }); + + if (!ignored && typeof allLocalesInProject[key] === "undefined") { + // ensure this key doesn't exist in the backend errors either + if (!BACKEND_ERRORS.includes(key)) { + allErrors.push( + "ERROR: `" + locale[0] + "` contains unused item: `" + key + "`", + ); + } + } + return null; + }); +}; + +// Local all locale data +allLocales.map((locale, idx) => { + checkLangList(locale[1]); + allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json"); + return null; +}); + +// Verify all locale data +allLocales.map((locale) => { + compareLocale(locale); + return null; +}); + +if (allErrors.length) { + allErrors.map((err) => { + console.log("\x1b[31m%s\x1b[0m", err); + return null; + }); + + process.exit(1); +} + +console.log("\x1b[32m%s\x1b[0m", "Locale check passed"); +process.exit(0); diff --git a/frontend/fonts/feather b/frontend/fonts/feather deleted file mode 120000 index 440203ba..00000000 --- a/frontend/fonts/feather +++ /dev/null @@ -1 +0,0 @@ -../node_modules/tabler-ui/dist/assets/fonts/feather \ No newline at end of file diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff deleted file mode 100644 index 96d8768ea9a10f004e2215a1a674287c8c7abfe5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31740 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 diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff deleted file mode 100644 index 0829caef1eb96f768bedf713c983e2a3904ad835..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28540 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 diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 deleted file mode 100644 index 7c901cd8450cecf93767560eecf4a3dc6b0cefb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22240 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 diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff deleted file mode 100644 index 99652481a07f237145d733e06e6b2c0b926dfafc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28744 zcmYg!V{|4>wDpsTCbq4KZQIGj*2K1L+qP}nwr$%^zP$I|A78EB-K+MhQ+3wqu2Z{@ zvy6xc00{8YY{UQv|2+>!e)|8z{MY;co2Zbm2mk=&^TTQXzz?lFM{>RUJ_F!rmtj4QmU zeBZ>NrbIlWialJE=FIghZq+0KjD}V+y!J zTLV65*Li7cn1{E@a^1*sy$OS*>~g5o61)*%p{equ!mE*8#wf`bsMKS$yT$vPRx@p4 zh*_QW&`tf*CtvLq3nMQzJgYa5md0eggU^ zB9Zi-fnUy z-#R1V9^|yQiNtq}#P^W~x0MBV+}<(0Iv=7Dzw{FFI^GfoilOM+W`m{P=tE+9 zBM5zGBZvZTr3fNT1 z?M;PjT2+1%)Zz2^T0Go?2dofby9%Ki`YBlGN5lDSPw_M&b>(ri{BIB2!L>FiXd1gI7=Nl1yg4F8C@N6CM!MDo<_l4VpX2O+@}-qG6R-DFw}?6!b9 zi^d(ISoFGPg!N|EsOupG}Ka(Wca{cNEMw`cm zSwoVbVquPYLECcmcWfS}!quQM{_3#L(pNL`fdg@)iEB>!F^oSLWX31yKYF3Ls~qXz z7%prw9H22#*0B3f26&z|xfT6;h{(5W?2#-v@u-O%>`zIx0?G;go=241Y_BP`WAK)drhd*+ zZ(tio0Y{P4?L!iDx^#dE_P{US-W-3u$aD_w$d!`wCfRkR7W^ZSvvHZ|_EC2joy z`C;~&^rQ_c{(xorUR^>DPovPrpPu22*rqy_A0a2ypc<4N~F7)=&s!DzIYj`vvW(TN#iPbbDccC=r z0c&gnb28>=uc3;TFX?F3uX(G^8Io;p$LZR}l6JKV*o)(*=7YF}S=TCdqWx}H_1DzN zjS2jwkFL{Hb#)z$q~2WU7qwMM40JBqH^EFlq*p|4Se8r1zeoQxV$Y%(H$~cWWlC=9 zH)lDk%HbN~q>)gyFsjeY|MI`rs7$>S&VCw+eoDAC4jY-L5tUQ((+=V)2Ww<-owzvc zV@`gPy2=~whrzTmhvt=03$Jlye72_6H)?3(zo8$d3q!ME{h|@8aQ2^fo91WbWUSTJ z`IJ9$Yd1D!;%5g69tQ@#B{#8Xde9(y)_+r6bj?y2bn1ANR0?}kdov)f^?yM_k^PJ^ zi@3jljazTzeUk5D;X%kT&c=Rh*J$HyxX!L*uCCap?1z;6vRbqD>Sumv2}5F&XYg9` zvj0<%4??T6N;TIfLWAXY;^7G`z!yZM~MuhM=-Trx}=TlQs28v)sDf=mLn|;ou5K zvjNxc`^U(DY=6svs)y4hkZIa48x82cO*SREAF;Y+E`}aTdX^2uCwSZ zM`1BLVPRQTVA^gDv6yuwjylt?+8X73mzQmNM%iO_vyH2L9CM*TR2_;W*2RG)^~L&(bgHBC}H?vkI}`~tF9Uqx)FBG4Wep(PHhbE|UwtqKth zbjCnV#xM}0-6xe3D}FRP@e3+M6~{^+rk&5J9t&qF#Jv&$PMrw%Ai&CM#cFDw>-e#~ z2Q;8%M7Y70dO0`kycOthV!9~U+I*ge6A~>h|7FFi_-bft=a}9jZ%xG zj%MdqjZtI{v-x&fBY1FUInHXJtq?slA3vwV1oRYrGOCdoI}XTq1$C((j>kfE$TWEN z9l@$=(LGbL2~S?D#6A5aOjIr~3ui!~m{v63FO;019uIufH#xyVcSfd6pHp#AQX80yG{* z-f5+8unOD+^h|~((lTlqB{mAjMT64e>5+Xg3g}a+sr7ZnhE;b|idmIo^9rs`&?x8> z49FNiI8B|g&oLI5_RX>nEWx7PH~e$Np>6!kvK5qS)DQu8OW?4l-~m~D%iR_1^$~)n z%cmM!GoqYTCM1(bDkJj-KFWjE^P`_Eqk-*Ib&*A;Uv(^C%$zl0#`rBs)c7p)fod%) z2~{;MjBPx5@nZ%!-ypKPKE_BZi)sp^+6Mp^7uW|iX!}!A#DqyMh!SUK=B~Er<7trL zu8!#D6ys2@Ht6T-=IJj^=s*Y|J~r?qVDKSCkt$5HmVVRlF(ALX#q1a8hcj*F$cr;= zm)Oy??6v|Klb^-wH(@z1>(+7IFYC8~f^lqT2!8cWe*u?Q5*!{V?7c*o5ZUqxK~9g5 z2-L--`|wc#;W)y^_*i9ZcyQVySvZ4e+D0-F{$k${!p+_gvze#-j|W{fz*t>d+4}=M1EWEoAZ|U|rUz2Yo>U zWzmCnpWnaRRDN07nE11h0TKA{0fXi#hyVEvPdy|4(Bp4DF9pf63QybStRAUI(R;VR zeLE2c@aFS4YdS^PjdB{od#NJK0p$y>z9d)~AzM#1%~sfZwMCgulCz-=F7K(0uYvz#o{^5lP3hxy&tzVm-5ht+R4^ z%xYKl@^rV7;y+QNdXOn*M#bzFRq5$00g?aoS6oOqf^09l9&FrNiApQCP-OP~1|a4P9P>Iu5)OaOv$uJ9$5PzgRyhKN{Z~ z_b`6VY~?4!4+0MNZR}BY8aEb5&j9>8OAjil#V`R&&;0qNN+D8XVL(*tN-i7x4oi>- zch+Z^=?FsFWsEWHF5{>p4};dES90wp%E=>#DBE;ev}<0HANtSlPLu>M$X>ocqnbWC zF;wmo_Uh?K&B31z|~GWzg@I75kX3PS^Ctb|PIu~Cu3 zoNj?S)zXnkFU-3b9>^h065Xsg2AxsWoC7LF$TAy$Ajm-zVZ%H>?aYri! zf6hNG5d4zb^I@zFN7R@ zpn;A6(7(Wb!1DvIAH@7%1OWZtKGpi`Yc#-z?6F>8c@J)ws6{!r$_9Y+lP=*~Lw?V( zY|TIAP2kg=wRQz<;`6F-qaEGc-I<)2VEprI<&VJ98y-Oyeb{&P@2ev;1~Kid@ZWtD zN{>+kl;MoyLlTUV6=me3=c3lKpeR?Y{EKIwmV%}{MBwW_=E5_tS=aH)ig{df)O%V8 z1etDgt)%#@9#k+$Q%i5r*;AJJWUZE;{>8oG6gU!gpUag~UDZsD?~11S9`$a)=^GgM1Y7H?ROwf$RCQ*>)B^T<4Tji60Q@(?9waf*ZK{E0 zUHC3R(tzd~0!AI;?M^$8%JSAam~5_lWmj6nO$zf-{E4`w$Th z0}+-3A?G}A5fi(|Y=o&dEBuIxLuOZ?SkM5)!^U(mP_U*oi5vQHm;(Nn^&4y|ZdKM; zaW6L{HRSOkRi{C_5oKpMYq(}zFZ6n=v}wJih;UoHC7bBoW!|UqiI+EH4)1sHQhdhG zlWfHr-DaL%sXeY2%R4m+=h%1g9WCF_G`9SDx~W_^KvphFfQ4trx;iF$k%;|v>sWn( zgEMZf!0Z52qxU!=aopG7;V-e%tr@ULhC@J7UUf{_ln3tm;uYu@``(Thd8D)F{U3_U z8>e4ilar63W3f=pmfBU5Tl`w-nGk45IA%k1Ybu&M+%Hw7T@F_7;Yf`e@H^y}9_bPa z-BEodipM#mF^{xkK)AnH%Khd@^Av>{6uX+GfRvm6_^*b9+Hw3s22iz0ql>;NgZXir z!zoLGuR%q5Sp2h(sCSMk;20L9a$bPX=Af5-59Bls3$2pv@VMLRbtr^~eznU*Zc%-( z2l}W8)||TTA7_mLlB;9IarYb*n;UiMOREW5uDKcGiL$1L%$D%z ze_+NPmRe|>eU9*ShGxE~65Qv#zk+d}yNf!tM(^hNz3%BI<&vEN3LFC}r}%za2Y8Ca z@9eM!d4eJMs7nP}lRYpUiCXB1wW~4yob?9==N5Lat@p5tt0x_*)66D%Q{UH++z29I zakaDgLc7XPW*}BNg=(w(rX%#=fKT*nBoQ%l(s3+N;VJ+xh|u)>j!?8F_2{v-Myn5k z?e|{71(UITByw@NyiseKMM*~h-NWu9K`(EndC$&D4JQy3wzfWHiv+9$bAa1Q{ zrT;+9Wu%XPUamI-ePYPTfA687JsI))Ylls;I|IA5Jkdz{4sv~dM0iDzk?EWe6Z~>yTzKkR zcH}UnQs#0G)rX4N$Y6=|8vD*?dbKAt4}`0|h8v};){CddgWH>?x=U5mN5zWtpXb1h z8z+cPSlRLzepsJY2wjYDbQFvG?v#HELrtS&`;FN$3}*FSkg_&mg&zof;;c&Xk9rmVr_njY|X&*q(GlAwH^bSS#-^<+d z<2po2+{5P_Hy<BYPSNt0|W5-^O2l!nkA@^pd|qhw|J5|Ee@$C&&wh+va0A)WS|6Yg@K2xM6IW zZNwA@+c2BBvfW!py}qTQS@z7x=w>p~nn>^bqtw!}oQ_w5-N5Tydv%p+Ts>8)01$TKuqK zJY^k7K~^)oUvy2klHYug&%hR}`GHAA7I%h+)wZzyK!*a%Ex*d4Ar1XEDn+U9KK~YH z>ZuqgHMeGCdNec*rUdY)Vl;Ka>5kh5#MQ`l4BufQFv*q2Y}6z z5Bd%s7-sK0!$iQ>3&JtS7Z}xEjM|Bz>9@N0_ipC5)qpj_*KnqyrH9A+^$rr0?4gij zM~HQ#g?ZPQTG$|)({oRUc)2gTZTN!!iWt?lhH83x^@+$I&KJd&%YTnC$d^Br5T|+H=jQjJEqtY<4i9GSZB@s3!@LL^ajuI zCij-iwUNZNhUi{!=da@{*4aVbyEW5n5hx0V^FJoko90wB7C_ip7GGl$ z2*J5AbRYBkya-#CeKKc(j+*qwr86c*@B8{Ut869H&$q0-fmChHUFk_4t*EEk3Hd-- zBlB$)(M1&rYDP7aIXTKNCCbXOWWsXnmH!&UN1*??4lq})xYsgaErEsw6Qq;8qLLCd z=P7%$%1cjL-rW*V>eFVUFELz{Q8)DzX0*au<*}|!rpzJcH}S z9!!W>x%^fI^F3THT?R&l-CRZ%cdwUmq;-=ey&B~ED-8R5=VB<#5nBWs(jp{55txBQ zRV}bdJ9nVQX+0DJ`FM{r!eky@GO0RyHo1FjW{WmRUF3UnQHu>F6aH(yJJ97#&J=GpLGPdPl)DuPCiSHN4_5bgPpQM9 z0T1(u#Jau@86MSR@ZD8Nvl&}GyY0K4^T7P2lT%dn@la96QU(#lAv%h;@b&tLpR{kO zl(pU*flD5m=VT01Fk8uPthDAV<(7)%WonFEhqXcn94J{aYP)32R=nZMGj;76GkrUQ zUEkyS7DjqBg=^6F#Y(sJGk=gcLP&4`DV73^oF2#*FyV|Z;>o|v$}k%_=9v39&}yM6 z5Sb1>;WA=cKvVdP^gqyqhOkiLurc()rGUk;L{K^k0qsL~M&w+?$q6e5=R~$}ev*&s zAw=qc4-)4d?;Q8Tk2zT3bFGZ!;o_C=l#a)LW63y?Z|>5ow+a3Fhnc;V=|N8MjJP$e zh@ZsyaW{8Vk5(g*ha6bUK-J26#US>{S>aSIzq!dNL;$p*c`$pTqorW^C?g4;DHQr7 zt2yWc3f-O!mJ51SiJcJX)syBh&Zd>5FvUq_k}uJcW&ZZTk(R=dJjq;|sLHo^{@c5X zn0?zI-QaRyn~_g@CZnE_1?hRoyi!(8QX?d&jc7cUi2A#~YH$EeM3lmW#9nPMRH#(s ze=|&4d&2w|U50Rm;1fE_jQZT;FQeU))T;FJ%w4ATLaB|$p<)ENYwHr5;IbVzIdY!hOpYFte@&^i(( zkpKnob%EA~D`WZ~eQawsOVdM8pC zXDZ%_+kZg`$p_k7DZ{Vy-TY=A**b^YaQW0FUB&h3(FyTh5`YNytjm z9R%Dug;VN6h2pu|0Xv4pKK66^%U?37j=B|@4+sU{TiAEi$95Xsywn7RQV)G(Y9nQ8 zg~Fhq7inVYrI;Q|nZ+sgC7AzV59Q??e@x-7Aj&ka2!bRJ(kj$QK*OS28VkL2K-Un61h++kVbJ#9!CAnWvAfI7RHT}M$Owo zOoY9Q3XBQqhM-=}`%(li#PJWT3%8u+HQd2}))~J*vp0{-XN8egx41 z+P_zE;J)h=jtHPmtr)*i`Xyaf|HZ!ek~~Y|d^GgQEXbZ??0{It;}rEQ)4i5D!(p)s zl)?f_Cl;-jZIoPl_4|{o4X1TjDw_}edmP?0iiTL1C{yWfqw<(Ff&zIc%Ucq<%Y}>a zg7H?ClQJBWi=luZBxDArd3ZYNplc@kQ%Uy6icNe(T4%^|Scm9%eyV?NM*$0w#GZm% zd?=bd3GWhkb^vsDv(98s$xt?X;R>LWr`!E{nl6fs&`s@N-hfPg}?fzO2kT73l zObYQtQpoN)3FE}knhM{39kQ%ySkH9WhE4f)Ck^KXP4P5&J1mn(gP6S`uEGJQ^##j zF#@!S{_ozui-)kr?}>|(TK)WY&t##O`I@Dwq7K(!jUi_XDa-xB>JbfYfSEP)c*lBxxf*rXJztVE$X@E2!E|KZ zvzgE~739qwIs zr%ff#ib6QDlRpXB$UxaA>aU7a8zv)cBsVdD(yG4OKOqjOtGr7L)%J}3r{;TFge{WT)gxB>f4KMqP?sDU<_`$)?Xh+i;I)1dhVv_bWjl98tEM3*@V9AZIL|^2<-aQL+TR zB5dXKml*rVAo>T7)rRjwZw1VR7Bj;YT9lOxTn)mLQlt>?9wi?&urZ^OOl$HJSL zcdGOSClAdWMdunPck@83O}aM!i~)mJqs}2`*NBvU*)E1Jz-zz z4{Wd6@p*+gdMVbQ&HG1;6VBOU@sc!CNcZkn4vuT}bo50a@5EiIFU&6?d4FA}d*YJW zES?(4eLwd(7MgjCfl0()39yIPkb9%R`kN9F=s`4Bkl$RSlhWGHU&y3TOw`U&{}L8- zD1EF$99Cbi3vz@ZUZvZp8EiVVBj~JX?}#Su43uM-FYAo|q}3`p{DMk#9oIOp$9L4T zQ2gZ+$ko*DIiI}9rd6$8TvpcUz{Z927S8Tpj*J0 zjxhswhOv9$BDoGD(KSrH44Ymd=o+4>7Erb_tiQ|RtWdKydXeQqssW^Vl{V2|_k5?e z-JUW1Cf@E}Z2@{Q?zHN>S9d+on&0n|c#+OC%Nj^dlx5U-%jE1=05>N}Qi)$3r}fri zi*mJy&kHKEt7OG|`$#N~8G&2gkR7m?Hw;twB&H zmNra@MA1kn8G{cb;MbY{sS!s+V6{NCvbVizhk=$nR5lW|Hb{Q8;Sv8mto`D6*;l6O zpz=^=*dC~WgcYjI*%GfIHPBoW*+GD;yC=Piakb-645jlF9LNolQ>jNZ1mm@$M)9Oz z9$EK|Rg05>vJN!l#5Jf${>-48%ATf?tKYh&w`DS4%)4ECte-wwjT4x>4$C=YKd?{c z!C?C-ED-y7(C+Mc*e0xJ+U3*6(cJC4NU|)Za51h9@SA}!pPI%=+*T=^K>S9OfqwgkQgReuf&QG!Uo^UTOFoD&` z;KdE&Aq#_Ri2wMBW6& z`o(IK16+%LCo@b5Wjph$MyX{TIfiQ^4y{BE&q>4G85~;^46U_=yUUT-lE-z`sps9E zO2We3jdpGJM!gMU=x}JBOF%bhcq!KtHh_(=u7f#KtvrA&PcK>CIagPV71ZUn zkj`zXrbh&paddC2dYv8yj*sHBS^`sJIbBI8ad?vfu2qg)AjsqU7?;wimg#bndnHk&UKWGk=zm#<$)V|j~jyz#yHce@3@;`*)y zz95hLUdMkQSL@U8q8?^4nw?cnVl$yxLPn>|r@gj~pFa~uPc0{s5#hkyZBz=TTjwwT z6{18yWHUo(De~|&2utoAHA}XPqc5d2am@*a7)wh86nk0NbfYI9|wkwXt$}BCyZW!PKyvuujLbc9f{#C zRezaGkdsz-^C*@MD~?$cL+Z5`ytUK`pV?!*eJE4DauP68Ln*8_=9*lTkwCFLvpin7 zJ@c7O^0_PR5-A(&i7SooTy4oFZ(lV!TdBfFus#bhgoanCx1FC`pXi{TZm(^&TZ)G~ z3+vj3wRWpLBo6P$rR=jDusPO+)jzp`F59v(#CX zWqP&ZS+wub@;1lQwf15Y6N8M&EjdUFH=M-E89wb4Jv`VDNYfwweXE6F4xI8I(FW{2 zMdHe&H2yF@>==Rp+X(U|E$cyql*p1S`UN3kg?~iFZQzSco^VLx$jzp|$*gZThMomb zR0{vYaL|AZ`_sJo1Lpe-hd8>xjyyy6S<240WifsHt*WmK@7DNpyC9~tfdFNmU| z6~DQGJLf!s*5Y9xa8j9Uajsz1_>+Z`qvq^-N~p^Scs65)m~|6#$06@lauf(_!Pv;- zCgpnPf}Htu1FH+_!fQQFDh;{RpX5eXNZq<>=6HLsIcLtZ@U?>+Cw_0HEQw&dHduP@ zo=8p1V4suq;$1I5uMU2+>CYRoVZGzZ6yP!1lbJU?AeHvu-Hi5-x8q6bmeBEK0|E6; z1S7saVxd9NY{1L-P5=f33Zyt?i%qV*kv71g5UDxT*nxWaa&rsK@rk{zfz<*iPxS{c}BW z8DsnLyNsIQwueF z>w(EnKq@1RIn5577h;&ZLIF`HRuiZXSlh68!)(l1oF2jpk-%zJU4Pma4OhJ` zJMQev_*djIC^vH#1mcH2R^JQ?W<^dNle5nm-{(S06sFP;?wZ*f)s|uORXqd|USYU@ zelW8c?ahzKP+UF}X4AFozM5JabD1q*I2p4qwztkxk{Q*3 zTC2Pus4~8HIOO8yIy1Z(E#gqqtB<#?42e+2{>6U}x~(X>RVNa$iKO={Jk|9$=I`h1 z`Ars#DPv0gLM@0wxysj3c!x@R)*HS=6Qj3_uN6myhcViLg0l-TC%XXUt>Yo#L=P(y zQv9$Y8*4RZAtoAom7^dw3{t88Aou{7<7(0peJ02hX!E-g=J0(meL*CnuJ!R2AFJPi?LlrW2u1%-;jWM57=qr~h!GqWn32 zu++cGlL=oTB|okL{_qmkaZ!#~Qf@WE1*c=9F8=u*(p+M2`Lby3xThvfMr)18rui>K zc|tGSYdblTulS>s_=mQC^I7M22eYkCvb#UyU+Ke!vXhbM`^p_Cq`|zFgz`%NZ>}x+ z$+gm#(KDVS#Zn*eo8caMQS(%u7_akw`#;iHe2QQHIx`2w0JQ@?h{fgGbtPkC1E{30 zKJu_ahJA+Lo^CHLacZdJ_PD5{mfW& zVQ{^vVS9Csk3yjorTvSwQO}rVLDq#G7So#}EmK&CTonYZ*IAOf2&Y^lsndJY}l@IaOSLcm)q2- zsl+=n|Igy=p}E$Ae7}=Gv}(q#1!H*S$Hl*uJea7F$JLv;zke2j!B)J*-x>nJRbf^Q zWXB%;KYcAm9G_fk568iRkaxdS1@j|n53X_(y`TkVfXt_^h4L;Wm0O99Ih`mQ_U<9e zWd_DKL=@&2`6RM1Mo8A%Ox4u73pbFam=;`*dHHuLPsCR}619y-cBh+|R6Qu?j&aH1 zQz2}}2w*#p7uGC}wag`Vo-+p9DYVSbxr>3WRD3B$04u3`?A22Ba~KO!?&rwQuIBWF?kS zmCz!ct@z~vqIH6bbWZONpJiX*McsDUsm71kt237tuz3yMg4{4WuKi zr<6>vYNwAvKKBKWn*?u}cN}9zQyYc^!%pFp@M0b&7Jw@WZuQ?m6 z4is*{UXb_qpnX*HIS(*B0f5$@9hN#zexPrkv33e{;9B^}bD#jZ9q!RLpi`_r5>0j~ zL*N}|t7nr|KuhGkw(zCK5&2PIrvo#Rt@Kk1nNEL^{PUIf-*4!62C#02WJc-8gi<7T z$Gw|`fnK~`*M6!>^hEO=wmIXG>fn~#fxkc=Snh)(^Kcn%jqI~v0r$d?T{-SJ7b(qB z-ACC5Dhgv6+LSXuut@n8R(zv$#KjXxUHARlSy?JVqgl03Jy+gMr;;Hr2^DI4THK$s z*XwT$XGCHPeN)n78l;O<(zeHy7IWi+eBV;7T{!86db4>C$8~O8-BrY-akcTdDn8bu zI;5oXd7JH$U_>a5zElh}zz*w9jY=Zx)*tcTuv+q9ebTp6$&5uMEVOMs{F z@;_0D_CRz^%g&{{#rscMSL1sNv`+oRZ1j~pczy8rA~yRTZRA_HI5wN+>9f9xfFk@1YAj`4AhyKEFoPR=l^HMnJb-zg=!n^Kyghw)T(8h%M0 zb6Opxf5eF}&uVK~bs|zXK7>1raN5KIe;r-DuCduLr;BRlA@RHXlCo!AEH1F!*Z_*U z=ARG|rZ|yNjp1q%1|lhJe~@ndL~}>-;RQ7#BhL0AYLs1rL(R<-dbc-TxATMt)_b*8 zBWc{_fvui&v&2vN0%lQbQEBl=Fjigl%z0b43w9|X7iZ9h`*7D%Nex$wLwYNqHytlD z@oCB+zxNHcm3J#X1U`PytwX)V;c#Hl{T=i6_R>&?MKe8V6JxYU)#z97IUj<<4X<^5 z75V_xF}_G3lLfeye%v_dxMQS%vmzv~o_~tax@c#FGi-d_u34JN8PDQT7SWpVe%t4? zZ25%HywE-M7w%rJelN>?!Z4i`rqLfl-cZC=s78)&PW13U2_G`+8MKY`Iur-vJ0I0*bR;Twcr|Sxto3awJ!FylR!MaH}qgJ}zBq2^__0Y2L&hVHp12vr&(w z;uxx^JnkXugFD|$(DBy71){ILnj?1+kDfflvVjw(7RPT}pQ|gt>ThSTlA($?Kr#mn zA5AZ3l2Jme9(vW zvxG>to&iuzMgAlFV(!143B8UTK=U#Z7N>8^-jTdwDG??|nhkQ0?!~pY1j>thZOwSZ zl+H+4dw=k}=Fnaj__hubC|lxX4{h3xSWBy1U9ywN|rusF53D z)FcWre7Q0iLI^A168v7{vnQ#hbj=~9OPzjzKa0&dmSd$kmz!5wP;!L zq-E2X>$^>kE+DF7MTa$_k}KR%;*2nCtuBIO$v+QPUyn#_iBvkq(I(9bvXJJOrqz5k zf;#S-8* zTC=8bC_JcZwWCuOSyae%^NAU&SS8`Tz&A1|*MUm0PMm!<5n9!3%0yXAvBM^UlXIhc zvZ_uBl|g}owh=+-C48oy16_fXnq!cG`pKuSBhHbIml>=lRN+EbDWayF;9%v|H?&nKHf$Q0vdaK3D$#c1_+k;@vbhgC8?s*r{%jPP>*LONxkl$KO zwdUy?cY!g?2=($P$=aLR+&g&gOZu9B%1KnW;_y{Z(%(Nat$>F`kB7TMi|S-EH(_}@ z-3~74qm<7Em9*VUzK&S)U;TL)j5Pyv(LQSS{D~CY2(V-QVB~y%_B{GUXs^(9QxHg{ zV!+!s`j8CZaCYkO_@&M(0i(OWCd7D@CSj$#4c;t%9>{6@iYHZrzOimOE?QRt76j<)^*3@c`>t(Q|Yml?Gnh>p#yHI6*UwQh3f?oU>?XAM@qL}2M zTDM^*D4vC}C2%71CW9x=sfBA&bsBdm<+h>|+geDQu4?dlB-XXMFh$T!FaHQNr-RxR zw|LmM#5miDxMPPzvL%jTinT^-GqA2%UkJrjstgd3t^_=GTA|9Bb= zPz#d>-) z%A1+~4(yr#t15Xu^EEUM^0u|?&cH4 zz_8??aiqciWPs$nvJn;zX4Jdk z*dU+X{fpRkA8n)U!kM5O#E@jMpDt2qmKsx^r@q9bXHv4L&SgHJ<5n6}qWkkV42aao*f=+=UQW=;*aEVm`org>Plzy}MXsj3KOJ=Cpw7}m- zqG_<*rjr5_m5+U3ilFOvQAvUUNokufO5y@aiv1ngiu5>0!Edhp!- ztv7*>toSy(@Q6X(=`a}Wyix_AX9i~XV?T4r`qMZBK*NVYC-gnd$lAKsPsu>=Xs{hK z8{gqL(UvR+0_S#VuE3*+bogog=2tV`CsG~sj7lsFu2Y<>%kdtQKQCc3nL6Uc*3J%3 zNF5d>>?;wq`JHRSS9aJZtD{~G$Jd$XLRopOTteG(%E{w!;eein%=3Nx$~a4XLIsO| zf6e)(iR1@`pPZBqpT+`A5afl;&)Ywp|98!bOQ2!1><%Awq0bWh0nXielkV=MXLGM# z27V2w)VriWnY?tjJBP;2dKI202hX?Q{{?CNg{9-(Ic_@cupZJ;I}mI@U&7aNMAxDRJ0V>h-GMoL5Sb{}ngy)eyoBA9tXljj zZ-PAUsAZkGh_f%_%0&d?ad`e(e0ctoOWv%8)jX`nU(;P)D3BTs&ClXZ+>el@?L9&U zH%DJ*)0_FF&Y-`(u_t}n86A4F{&3^Tb(>kOMI$DEj9maq1mHv2xW#ViGy=cBbXHcg4zaht9fy*!GfK^mP#& zK0Ut+)}k*%e>s40wkd=}yAYSk%sO7_fvCZ9kT`A20^?_)tgr5^P=VQUsRHqx0{@uc6 zC~yD#VSEJS#d=g(s@-^mF!iKb$z$#&)Y)UojM^-jFz_zebX3(cf8ixJc3tls4!^JK z(Al$Gqhq+^@yFqD^h*R=)BLOWW$q(Db;jrn5};r+9XCmc6`HGwjk<$m+iB~fGg{C^ zU*e}tMYiR1BwZdZ_dBJCRO)jD6SedvDefK(M~i`cZ$41ZnT1$2n4S@n_MU)0M}a+s zE@!F_Z}!2tti3B46O-YP&6t}Tk4Xh@Bqh0{0iRroR94tL{$M#`j|bAu5X_)(Fc_~y zYQutC3{+zR!EI{(FsG+Cz?hf217?YHA~=(?7WJ5M8GV??sXJ%xT9=-!OPQ5>%ehUn z+&>%*;G&QkPBwN8_~+IkC_6m=2ksv37Gy;-v|$}BHSwV?NPVT`vEiK0bAF|MX2~49BPT_fOvpVc+h{2Fsfp;e~ML%ye~Af6!+K z7Q~$$lHrYJS@6Qln8G9Yd6;2DepW<%XzI8cd?f-XAw6jj8=0?+uGV079h0X)IvB=z zn3&cR%hlv6lg$W{EHMkIB>q{jF9q5mQ%MLtzRFZA*q3yT)kC$U(-O?d@nJFK7Q8V> z$Ygg2?w|{gxKabr#H#*?D?JcNtnLqo#Qbo$zDmpwg?)vnz5cq-D}*$w$H(_Mx_x9+ zt;A=6l>TX;4jwdwj?mjn9Dqx6T5_~YIi}0Wvnk0lzwtWwn3Qui?qKRl0RxRM4wp6Jz~VZ$NdS6jkX(d=^#}3Ulw`%K2;7oIm&a z&F8%N<~cW$`laXh^IFhF8b|BV>{4mR5j3nU5*diuXlw#v9vX{4Y^K>Y5JnWQpFPWr zPnaxXLd;o=xy6AUjnV5)9g2@_FR!QzvxDh$*yy$R@?o3&P;$+MD-RsLYAUt* z%*NC?Gjd!KqurrmAhmHMUQGTV+w1S&(p%XvDmTu3V9UsXiEt#7#G3vm+}2QOP)t+1 zJtso=XWWg@2!7C!Ygb}6Ek5}-9bZRIAON8`?Ny9SAEBDT9du-9m^VQU8xbAMSEPzL z9mNpZS?FSLFH7g65?ra;{1nyfOoD!-s-(8U57D93U28M5#`S&0+7<9wZCsmKW8Bav z4ZXLq=3wUFTh)83pMif6y!F;wSamP#ML&pd!mD@-r^eTy8GI8!RsrM^UIfUsO$a;> z;)^KcItJMYX|ABTt|t(>mi!(!@LfO)BosRy9rU)-yc3Jh9XE;@zr_h!c+GG$>P`8~ zfe3vXgm^WW*|9Q9!t9O}*#e<&nf_=$JnQhPZ3tluwbRfYF)+-uH<~xEkdq1i;(@Ii zIqS^M;2yctVxGGcHFr=rh3`b}<#8}W0xcBN>fW>1F79UMA4N4*_iyfT2LkrERr2;l zD+8|lRPHHVG&xo)d&Ho}7WRaD^8Q+FFkB?>h4xFpo8!x`BHjf;!?vZY4out~YTckJ z3v~3DIGa`Nn75AW^P`ryF&UH!gCR$Ac8~;t&bSE3tn&v1T#LdAg z7YV(4+J0F5-Jx8sw#R<+1tzy@@yo|R{;N33NA;2ojmTgI-mjEB9vwPlpFTg}N>rNdVRT83aLi;BjuiK6 z)cs}f8|xqx1nM}9@1z!Fg=I&-vbAzREUMJfw`d-)ZPc=2W0(^6c`_kes5&XvqakC^ zkzJv;6vRT0AcJQskG%^t-Lkf{!5-_88heIA(WtR&^~!E{Jrk-%?Rle9virk=OWjNN zt;6tG1MO;n!PCk!5fvS&DFvQ%ZJvHfhdWf+Mq1SGetK0gl-{&yYnJetfN>r$_M;@~ zU4FJ$+AvMEs(+HTfQ0QwEQ)m`V5%-u6cMiD`tqpR6Y9nm!6s$hxq8lP_X={!Bl?3b zF%}hq1#s|dmLf4jZ^j<+JHa<;RB*;cb2$~QnpCE^&z%Zber$=^y?)8#4Sz)rTAbY` zjV0~!hsikZ1Z;AEjS;XZEIMbUxDbJhn)V-pn44l{Qs`I}wqeKUmO9k*O}bR%`j{QM z$47h04L#_M=b!_N%?@;vt_}7~eo1h5_OiZuGaB{GuI%_PYZ^&ZvIm~vS#%>Y(soFmPVDE8?+PoKDeMoF;OXAL4~ z*A@cgRt7PlhtVw*avOtez}Mj0xNmcv_;G{?4)A>+{r&`%UbElntw{AHd?ur--E^FH zm=|0E*juhc($LKbY}FUvY6SfwwArX`@Z}pJK{lw2>A`HL%Gv8rl+JZl1h>&=5t2c> zAO9sY+ZPg?^4u4*Zn16%ir2ero^(ABiy&^H=G5DM3@uRrfmNC9Xg8?TzL-?AJf$U7h*L-GO$yy+uYCcJe?Z=1lTJEE?9s*Kc%XWB9 zh@JSgU(eR+h^AY@!|f0Sd@k4GiCNj-D2EAo-uf<9-Rxivk??T zdBDD%ZPhPOCO^gAsJK(F%1uu%5CZRh%!yhjkwB0hlB;c1%SaPyO~9K?<=i#DzhIq` zrp$hKIazV{2O8ls+=ZAkouJS269eXmke>DRM+UoXHp^gQFq8l$el$F4bI4X#vMv>? zu2el#tRli`&)mm>)~n&N^9J@aKHF zMk!oQ%IThHWFX_^_8icr#IzXeP5aZboEuEapb*FJA$uIkJ=}t#X!v-0MqwF7Kx7+x zKXVozUf50Iek={*r4I06im9ms?x0$wot)eS4tZ4MBE#aSy<|!Is#7ZNA;Zl$k;a%e zWTZd58>(7Vcp3ugXwxrNEwm+#Frdx5unc$>YL{~1)@tpWDcXZ%zXa_ z-9udu>mCj~kr+$jk;B1}aAYKKc<%Gbu>?6quz}{32Kpq6-hP~Ol6?A4L8F6UU{BQ9)+@nYXnAI2cF-2MgRKpZiEmgrK$1PfR z^c-fcYjs&70(1ygBX5Pd);_^x9uO4BkbE$n81q#VuDpG8Rbp&o%9gi9%C@XUa*hQm zf+sMZvEx+lv>tEi2$jd@9yv|7Z^OX;$%N|U107KzZF8#Tp5mOAk}bfWN_GTapMQ+g z!TDJX4KLL?IfB#*>9s=4t?)(qCLLr7_j;;u-+pD4G}x}iXylq|j7HvXk9E8BZey(K znAl(#5d`3C_+sAG3R()(ijUzB3G3(RsgD zh=h|;G&!}q>X-axw;>dk?f$MFqxsPgXf{PR6#d+)Io{UEb#-*iJ+?B7zhfBKdcoY@ z{T+r2Z}gu$y7`{7Ml}2O;Es+nuQ0^~$zz#Qa{&Q{XDi(-qFu|;O^0$2qb7-IOj01-iHd2f6 zGrIdQcSw6xXCgxBt$%-itiu?v#8i5_u7&1mx^-!@-^is)Yr?8wz>V_EKjy9?XAD$U zl15)!M59cL@2@c}V{XR)egg0}n=FPf>ZA>2R)hp*cHf7e-9^vrr2!Iap*lNoi~DFPdpx=>_eKwwKF(>|=Amr+@$Z zkZLH{fzdo7pXgBZ_rL!=F3I3#bO`Z8j};$<*0muI3ZlJBHS12el!?lwSH%b*W_}Ag z6v?fMS-fCMW|+b-45sGb;geVmtw8oxJfZKBZ{wtPDtP{?J$pWdPnM0^p?d8+PZ*Al2kl-jDn-q z4ri;e1TjJ*B<9p)p?ka7YHZI!ISNnSaWn@$UXs>l0a4g~Ju zc%w7+Di(ie`qA7QF&`o$ty0JLB2tzWjA3H5N1Hc~pgRRPXKhW|3c zSS>r80Cj0)S3`b_va~Uhk4=N*!f zp&i%uVHIE1)O`n6#R?Z1pVbW%H~Ond0saE!kQ#K$HNY1V9!OE8>%_3`CUKK){mOzFM>$IMFe} z6Ds+Fb#irP=Hy;`)t2t*?lmRAzwcnAQTOGI(r)83WzTfpcGiC5<@+|@eb%UE-@eYW z1Y4EL&Z|~z-0L)dv@!QbZ$h;fx}U6=U^)0Rx>re}CAXt4<0+C|HTEBGO>WGjPK!^J z{XMqH+03d_daZd|yxd(i`kki-N`fq{O51)hxx>iWI(v6qHn*KrciRJJ5Ai$pXv{&^6Qx#6X-ruu{J8^ z)ZJ~C9WFwAt3Y|biL7qPuJh%^l~HG ze*N~rGuQ5cQ+|vG&B1&y-jNOurp<+5W@3wV?$?;Z1p64s$-YgwhI1ELNSYQStK6pF z^dxVi+ZMADvnVl#5)+h|TZwsCj3}V-<*LKTNmlhB2Y+zDZQ`mX{L%E7VQ8#L7(GGJD=s2!~>Y*fe}T`}kyGavDBlrWJpI9!g6Tg6#`HuR5|M)fc1d>7Wl)**_U%*?aCeqdT zFimk0r1&(vR|ZLxhox9aQ(TX4pego3inmz`fu*R>6xZUXXo?J^c%7!WPI=2&tc(XC z#3+Xj;MchXw4sO?*;7_Gwc-N_+O{l}i{+xdXqW8oeC*@(j~_npmEKog>HP{R184B- z_`nj~B5OHl&*2AgrneVhL+B7#pc{#Ah;|vCLW~u)2mmZ-bTxG%PL17tH~hu#xhH=2 zq4+}&#pqxBVT5?VU^ie;fS8G{5JZ&DvF)G(VrGafu2rkDCEpT<+P>uwMMukr)c!5e zVHZWaL&T^1nlT?~-5h!q@CFM?{5fV)UL@RkfZ1obfzo8LvANhw-s3~C7`Z1-dQ z`rJPJ@EccL@y4NNYwsVL&8hOMNozv>H(CBA%Nps3L0rz_a)&+l=H-{ai61^RygIMS z%?`c4h7bq25$jJA>RF^23gh+oT@0z%9mVaPtu6;yhyN5mc=cy$FTPm&%+)n|EAl$* z!7p-;lA12npP-I;tx^_CCcN&>fjb*RX+GP49M{ZU@!WOS5xu`OR^eAbrY^w$=3xL* zPKG*YU*S-j3(*C3c%>&DkJucNCFvUUx5Tl^6Yz*2rb;Y?Y=o21*#XEY=tJ3&a>4*ba*%fHkR=Z_Iwkea_Eninml~?Q{zV;@g zVp`uNt~`YQtM!dj%o!+o8^`G!ENW4=bYNxW`RVB*4H$7FR zm7}Ao5mU)HD8V-ex(Oe1oWiTg`6M*+H~qzsvC|})t*(rtVu)m`Vy<$-L6w%b_^hxL zI{Z`iWPAkVp3bVlksOR`Et+zd*MyC=wPjbMe9{}+ZLvxS3dlL5oaKArl`AF#qhmz9 zW&U5eB4>h;5?XfdgJeMiUr^Iwx1Y@5&uXno{`?*W{78`7N1COb6ddH;hRyVZi~D_#r1@4yv2V3Rkg{n8~l=a81M5I|GH<$WCfm!t}B`;PO}nucm6K z72E;0o5uBh+>pobNrn0PN(j@Esx|{P_$K!n9_FWzeMt?h%7ws64b-d#dJ64UYH+Lq zv+?WRh}nT8R{|8b3dTze#Q0S1laf6nbtW=M=LzoBs=s3 zl0Q!_`E-04YwRJXGvUA34VH!;S4R@vi>`mvl^JQdL$sRC2zBP4NP(Z9otq`u_f-Y z2tIqa;D3gO4j(sTw+aD=4Zg9Eyyso+4Q%EU|AY60N%`=3hK3H2d&6#Z1no9^a3Q2z z4p&D%0$+hf+7fDjbSkz5)e1;ciDvK`&K2R+YfPiVnhuwgk$sQArO+>c-jO}v(z7?m zvS)7|ypX;g6gYbbAKJI0FfIqb*4{FOZCSO$W!H0XN3+WXxi;vSML==LLe?^J-Ly8k~z^~v2hVzZx*_sqo{`X zrBG&b|G%_MU9(J?c}Od&$$$dOih3{SkJc-3$<3*DL9t$^)u!4$suf+i-2Y9PJW$;k z?BktuZhDZ+W@o!b{(pX+k{5f-Hm}WM6Q8w^(CYn$?x>cV0R?=Y&0_a0gpZ|CfC&eb zA%n!Kx>&m<_;p5cf%=N*|sta8g+`Ab2uWuKpr+wS5 zS=;~&|2Thy;_q+mb*2vI|1Y-$)No19A6yw(tvcO**3uTa{3O@-xO=P8|1)BQ zPnszeV5WquR1AU+S39>BWfOYJs67pntskD!XUT<+2T@jZN$2hN} z+r|OL-3W07dP`;iWtW_9EPJ~Z^8%D*w~}TV@7VMlkcQStg0Bb{5TI;D=Om5 zsF~gO`r;96T>=STlOz=0YrpcPAp>iiYB2Tl2j=3*Icx;eM!i?f!@H1>d6t%l2A?KycI)wL3%4F~UFgTh z{QcAZ3sATL{P!#UO4FO0>9MoL7cohnVCe~m%Qg8Ny5^|=BTk1*a>DJL2FnZKcut2E z1#yJyq3Z{fu$1qinJDjb23Zb(|Jefz!2bu5Qxb9j00961009$qqWvatUk^O>00{%= z00000(h3<)00000)jwRZ|1JLI1X2UZ00ICB00IC200000c-muNWME*v@$WqY153q! z-GA*YLCh~04ly7BCSCx(69@?ac-o!S1B|3e7{>AEt#55N7GrznY}>YN+jqg)j&Zib zvu)e<4f5^O{VQ9&87s*zPjRNZ)b0e8>j}xa{mX7+mAOi->b6mtwc|tTTTnkrWlE{P z;JaND`5Ns{;&7a*HJGz3>*(v!H0#!$?htKle`q`RkM_3T{3rIRws-R0sfd%{0uH~Ig<_Zf+#%zqxfb3d%Xa{H+%tjEyeS`CEtTkd?-hqWA<|FFke z55c#`eHa}3GBm$5Y3@;H4+j$Vbui2;3ww_Bux%{2g)=Z_CxWxnQ#eC|i|4B+_RY1b z%$=dN!?_ueP8El<*_T@q&R-PI?Qp`G9uVieC;i&X{Fsq{ujid8+zb7sdfS}r@Gqb} zx>Kog@y-nHYaJc#(=e3OljD)ZJGYa6Ic8UMPq>GJ;oKC3JBn(Q#`3try&bb6aNm0h z_kKvc2|c-KstfOl!n%H@MXs893pUbB`$DzI+ZX;nv{Rl$reqrqM;YemFJ<(X|FV$D z6WTt!WteX_YA?Idr}Uj;H|QMlC>%}sC>+iB4!gm3C{;BwcC)IHv2fRd*HxC7%9yjY zRjJ0l_Mcdp*0l=WxN=P*U$;^fldsqg{xN;e{n$afDCE^TlQVmueX5x*qfOn7D!1#% z{k28Xs0RC7Svx?t+rHY$_STlRpLVhXHIueIXfxVG+vJecvse$)F+pFeN?-XObcbJx zPnowa^Dd*Wjq$C_TRkN8&2R$N#wyI)RbMHuJJf`kjN8%nU~C~*<@0`_shEv%ScG}l zinV`(Tr0Lu#u60AY>s^!p;|lmhJqxcHQg^dIT@$@U8=@7Hdr?%dbEG8?a78vb~D)f zi!{+~t0jT{(lK^}jw$RT_u@x4-lmfe;a0qi*YP{HlQbK%up+jU{3rMguj4JW!{c|5 z+8EnX{u`;Jjj%p4sEfHo@;{*qr{lD!U6LC-l|`wZkaPjy_T&7f@%JIVK`z|;jcLnL z4!=F&y=f=ITgJH1AO5EI(*Hl*Art@rc-joX1E4G+006+VmG){|)wgZiwr$(CZQHhO z+qP}K8Jo@4Ea4VAv$wD>wLfuaj$V$Pj+@Syvxc*u^R_FotE+3TJB53I`;{l|sqLBU z`Qa_+9qHZg%j8?``{-}szYs_gSQhjK^94r*uZNWzk@$!I=Wjkcr1=sdcObK_#TBCdm*<4(9Q9)Tz0tN0;)i+>O|0YoP$NOn?~ zOeAy3O0tz4BxlJ@@|1ief0cluC{jtQj9Py=8rAcW%)`E3m1K1cggDqhj*dBI*U11N{8}>tWt3d6m-cX;YAJjkG z&j}Yi4bQ=g@Cv*RZ^66p0elRf!M|uZv_;xFZI^aTyQJOIUTNQSrykW886Av1#t37I zvA|ek>@bcP7tAVV1GA0U!yIBxFz1*n%q>=4tE5%cYG}2!dRjxR3Dz8Igu_Z4IY`9W!50bURX6x0IEKqt@-39J7@cQSW+StgImujQ9x`v4pKNxvFbh}*OIV4m#MWb5vE9sR%stI3Ecq>ytR=0- zZKZ5gZ8PoJ?e*+C9Uez3$4_Su=TzrD=PBnk=QCGPm*Se>y65_WGhr*Pi0k5(xD)P= zC*l=&3qF7^mzB%MVNT>)aU;3q+)>`fkK~u|+xQFoD|ZpM;oj3zFuYs?nuaj@9?}9(Szpa0k|4l#-v@N61h#DkhkPpFk>)R zuyC+UkPYG>3C4q3a8~eMsC1}r=v7z?Z;1pWQzD;JhefkQH^xfDM#cH~nE1m))5OWd zlf;|E7a_Y)La+&8p}x>bm?>Nn9*UVoDApExiz~(bQUv<+B==oJLxO*$A;bLWUMeA8{d=Y{~5`}$@SDjqtu`^XcO9j_MwC6csiRdrzhxR z`uG>jty#nX009610uKOi00#hd00jU600000015yA0ssMX00RI4c-oDTgKkAp6hv2T zgmrtv+O}=RN&Rg9ZR7q#G1<>in|m;4&&)x5F_(Uc^QT+xcYNtC%5_UtgFNCiSK4o8YOCZNi)Zal=+Ig-3d~(2dxb zc9di;+kuop>-T~udDM&3$*rOZsa|+RO?TIwklSzwiI z_SoT4sMMc#o0suY_aAkfGVO!S5fjz~IHi@Pzg1+$BKj0@OBLtEw8?^cf+f=jc*4h< zY2KNz3eV=B*Ir}=cUY}>YN+xGis+qP}nwr!rZfB^XM?$_%G;x_~Y0>MZ|a#E0zRHP;i zX-P+VGLVr>WG09#WF;Hf$w5wXk()f^B_Bm8MsZ3|l2VkW3}q=tc`8tmN>ru_RjEdG zYEY9})TRz~smC%7ae(DCHo#ysF}NZ4i>7=tBtHygXu}xRa2%l-2My0*BN)*@BN>^G zMlq_RHLPhZYg>o=*0mmwcxHWC(~35~ez^r8>_=<8U=`3JLD<)8lL-~Qvj{^$RWcY+hS z=OiaP#i@*Pn$w-(OlLWp0rY2}a~R}Y=Q-a6E_9KLUE)%gx!e`5bd{@J!&BF?*>$dW zgB#t%12?;c$2@nd+uZIB!Vr~cL?;F@iA8MU5SMsFCXo0fU-Jn1PeCQ(|`^2X{^Eq36;Y&jEj<>ugA}@H!MiP;b#QaMk8Zp;bzV?l8edl{W_>m%v z<06;%gUejwx}W^)PlDjVfPnxA09Y@zZQIy?wCX?k#5aENk3>cwD<`j@sHCi-s-~`? zsim!>tEX>ZXk=_+YG!U>X=QC=YiIA^=;Z9;>gMj@=_PHWJhar-fiQdvD`#=ao9uB1 zPIpc3xxH$Q04GM`$o96U57zt#w0-%;(==nv;5+G-*IG%Io@;R-oIy68pBGN5)=G+R zZeBOK9=5AiTut+(>UmuY*|VbN`=HU=FTETr_iC+p&q}hENL`xL)AA7Rl*sib&Mxln6Njz9(uvv&`G4t zCU5q&Q0g!N=U_^V0``tV-&vti3~K}?;M{pnMLl`H8RVMlVcYTnofiJd`;F4bQKsw@ zW&UJkj*%%&!5l2vXXEXDzS~_~8U{W}PdqRnE=u;rIw1+*p295$OZE%B*I#g-znJ?x z`9(K!{p6Pi`U$}poPi54bAF*KIr(MmoBg)d{6etbsFB}}jhz0rY=jnF)3HB{kNd~n z8JL&EDgqO5&i*v{rhgs=3w;Mv#=LSkI^y>5mk!6k)Yf>`$KhYv!(V_E6QmZ%DQN1& z@pOT-Yb)*g?$n2DZBM=LZthKe3}%zfIQ0$PPGe7f;W-UX`+9HcXOF+FwGgu9a@o|Z zrDIt*qVE@>SusgX--9WD>+a82uQeQzC5W)*`oaKUb99d7Qf0~!uz z#CY-Z>c7?gpUU^r;*pZ#tQ&USqyADEVcKuBAl>Oo4Vt7Iq1D+^s_hs+LVrmb1dJjD zkknkjuWPQzuM-zSk|>(>rYA?)AmP&;*Fv^pMTTeQQ6C)LozRV1QhcqpTW&M#F^s^N=qDiT zaR7#>KeGCc?3t*_s`?IQ+(}~qc-q^*pv|y}k%>v0aT7C$+|KM|tSKkVz@fdJ#R|$| z*v^>XVWTDn5@hnQ(NO`ifVx;Y*tIt>D7e;UO1OCMU`Pmx*uW^gfgy4OV;=y$!V+Hq DnjJ_~ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 deleted file mode 100644 index 343e5ba8d3f924aef2e589f25657cb97ff423ef5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22436 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 diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 deleted file mode 100644 index d552543be16ee93a7c66358c9cc80540a3b11860..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25656 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 jcp0p9&3Un?g { + process.env.TZ = "Australia/Queensland"; + process.env.IMAGE_INLINE_SIZE_LIMIT = "20000"; +}; diff --git a/frontend/html/index.ejs b/frontend/html/index.ejs deleted file mode 100644 index ae08b012..00000000 --- a/frontend/html/index.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% var title = 'Nginx Proxy Manager' %> -<%- include partials/header.ejs %> - -

- -
- - -<%- include partials/footer.ejs %> diff --git a/frontend/html/login.ejs b/frontend/html/login.ejs deleted file mode 100644 index bc4b9a27..00000000 --- a/frontend/html/login.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% var title = 'Login – Nginx Proxy Manager' %> -<%- include partials/header.ejs %> - -
- -
- - -<%- include partials/footer.ejs %> diff --git a/frontend/html/partials/footer.ejs b/frontend/html/partials/footer.ejs deleted file mode 100644 index 7fb2bd61..00000000 --- a/frontend/html/partials/footer.ejs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/frontend/html/partials/header.ejs b/frontend/html/partials/header.ejs deleted file mode 100644 index b8d88331..00000000 --- a/frontend/html/partials/header.ejs +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - <%- title %> - - - - - - - - - - - - - - diff --git a/frontend/images b/frontend/images deleted file mode 120000 index 37c31854..00000000 --- a/frontend/images +++ /dev/null @@ -1 +0,0 @@ -./node_modules/tabler-ui/dist/assets/images \ No newline at end of file diff --git a/frontend/jest.eslint.js b/frontend/jest.eslint.js new file mode 100644 index 00000000..04ee4f2c --- /dev/null +++ b/frontend/jest.eslint.js @@ -0,0 +1,6 @@ +module.exports = { + displayName: "ESLint", + runner: "jest-runner-eslint", + testMatch: ["/src/**/*.(js|jsx|ts|tsx)"], + watchPlugins: ["jest-runner-eslint/watch-fix"], +}; diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js deleted file mode 100644 index 6e33a6dc..00000000 --- a/frontend/js/app/api.js +++ /dev/null @@ -1,757 +0,0 @@ -const $ = require('jquery'); -const _ = require('underscore'); -const Tokens = require('./tokens'); - -/** - * @param {String} message - * @param {*} debug - * @param {Number} code - * @constructor - */ -const ApiError = function (message, debug, code) { - let temp = Error.call(this, message); - temp.name = this.name = 'ApiError'; - this.stack = temp.stack; - this.message = temp.message; - this.debug = debug; - this.code = code; -}; - -ApiError.prototype = Object.create(Error.prototype, { - constructor: { - value: ApiError, - writable: true, - configurable: true - } -}); - -/** - * - * @param {String} verb - * @param {String} path - * @param {Object} [data] - * @param {Object} [options] - * @returns {Promise} - */ -function fetch(verb, path, data, options) { - options = options || {}; - - return new Promise(function (resolve, reject) { - let api_url = '/api/'; - let url = api_url + path; - let token = Tokens.getTopToken(); - - if ((typeof options.contentType === 'undefined' || options.contentType.match(/json/im)) && typeof data === 'object') { - data = JSON.stringify(data); - } - - $.ajax({ - url: url, - data: typeof data === 'object' ? JSON.stringify(data) : data, - type: verb, - dataType: 'json', - contentType: options.contentType || 'application/json; charset=UTF-8', - processData: options.processData || true, - crossDomain: true, - timeout: options.timeout ? options.timeout : 180000, - xhrFields: { - withCredentials: true - }, - - beforeSend: function (xhr) { - xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); - }, - - success: function (data, textStatus, response) { - let total = response.getResponseHeader('X-Dataset-Total'); - if (total !== null) { - resolve({ - data: data, - pagination: { - total: parseInt(total, 10), - offset: parseInt(response.getResponseHeader('X-Dataset-Offset'), 10), - limit: parseInt(response.getResponseHeader('X-Dataset-Limit'), 10) - } - }); - } else { - resolve(response); - } - }, - - error: function (xhr, status, error_thrown) { - let code = 400; - - if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') { - error_thrown = xhr.responseJSON.error.message; - code = xhr.responseJSON.error.code || 500; - } - - reject(new ApiError(error_thrown, xhr.responseText, code)); - } - }); - }); -} - -/** - * - * @param {Array} expand - * @returns {String} - */ -function makeExpansionString(expand) { - let items = []; - _.forEach(expand, function (exp) { - items.push(encodeURIComponent(exp)); - }); - - return items.join(','); -} - -/** - * @param {String} path - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ -function getAllObjects(path, expand, query) { - let params = []; - - if (typeof expand === 'object' && expand !== null && expand.length) { - params.push('expand=' + makeExpansionString(expand)); - } - - if (typeof query === 'string') { - params.push('query=' + query); - } - - return fetch('get', path + (params.length ? '?' + params.join('&') : '')); -} - -function FileUpload(path, fd) { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - let token = Tokens.getTopToken(); - - xhr.open('POST', '/api/' + path); - xhr.overrideMimeType('text/plain'); - xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); - xhr.send(fd); - - xhr.onreadystatechange = function () { - if (this.readyState === XMLHttpRequest.DONE) { - if (xhr.status !== 200 && xhr.status !== 201) { - try { - reject(new Error('Upload failed: ' + JSON.parse(xhr.responseText).error.message)); - } catch (err) { - reject(new Error('Upload failed: ' + xhr.status)); - } - } else { - resolve(xhr.responseText); - } - } - }; - }); -} - -//ref : https://codepen.io/chrisdpratt/pen/RKxJNo -function DownloadFile(verb, path, filename) { - return new Promise(function (resolve, reject) { - let api_url = '/api/'; - let url = api_url + path; - let token = Tokens.getTopToken(); - - $.ajax({ - url: url, - type: verb, - crossDomain: true, - xhrFields: { - withCredentials: true, - responseType: 'blob' - }, - - beforeSend: function (xhr) { - xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); - }, - - success: function (data) { - var a = document.createElement('a'); - var url = window.URL.createObjectURL(data); - a.href = url; - a.download = filename; - document.body.append(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - }, - - error: function (xhr, status, error_thrown) { - let code = 400; - - if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') { - error_thrown = xhr.responseJSON.error.message; - code = xhr.responseJSON.error.code || 500; - } - - reject(new ApiError(error_thrown, xhr.responseText, code)); - } - }); - }); -} - -module.exports = { - status: function () { - return fetch('get', ''); - }, - - Tokens: { - - /** - * @param {String} identity - * @param {String} secret - * @param {Boolean} [wipe] Will wipe the stack before adding to it again if login was successful - * @returns {Promise} - */ - login: function (identity, secret, wipe) { - return fetch('post', 'tokens', {identity: identity, secret: secret}) - .then(response => { - if (response.token) { - if (wipe) { - Tokens.clearTokens(); - } - - // Set storage token - Tokens.addToken(response.token); - return response.token; - } else { - Tokens.clearTokens(); - throw(new Error('No token returned')); - } - }); - }, - - /** - * @returns {Promise} - */ - refresh: function () { - return fetch('get', 'tokens') - .then(response => { - if (response.token) { - Tokens.setCurrentToken(response.token); - return response.token; - } else { - Tokens.clearTokens(); - throw(new Error('No token returned')); - } - }); - } - }, - - Users: { - - /** - * @param {Number|String} user_id - * @param {Array} [expand] - * @returns {Promise} - */ - getById: function (user_id, expand) { - return fetch('get', 'users/' + user_id + (typeof expand === 'object' && expand.length ? '?expand=' + makeExpansionString(expand) : '')); - }, - - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('users', expand, query); - }, - - /** - * @param {Object} data - * @returns {Promise} - */ - create: function (data) { - return fetch('post', 'users', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'users/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'users/' + id); - }, - - /** - * - * @param {Number} id - * @param {Object} auth - * @returns {Promise} - */ - setPassword: function (id, auth) { - return fetch('put', 'users/' + id + '/auth', auth); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - loginAs: function (id) { - return fetch('post', 'users/' + id + '/login'); - }, - - /** - * - * @param {Number} id - * @param {Object} perms - * @returns {Promise} - */ - setPermissions: function (id, perms) { - return fetch('put', 'users/' + id + '/permissions', perms); - } - }, - - Nginx: { - - ProxyHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/proxy-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/proxy-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/proxy-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/proxy-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/proxy-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/proxy-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/proxy-hosts/' + id + '/disable'); - } - }, - - RedirectionHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/redirection-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/redirection-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/redirection-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/redirection-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/redirection-hosts/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - setCerts: function (id, form_data) { - return FileUpload('nginx/redirection-hosts/' + id + '/certificates', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/redirection-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/redirection-hosts/' + id + '/disable'); - } - }, - - Streams: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/streams', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/streams', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/streams/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/streams/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/streams/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/streams/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/streams/' + id + '/disable'); - } - }, - - DeadHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/dead-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/dead-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/dead-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/dead-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/dead-hosts/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - setCerts: function (id, form_data) { - return FileUpload('nginx/dead-hosts/' + id + '/certificates', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/dead-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/dead-hosts/' + id + '/disable'); - } - }, - - AccessLists: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/access-lists', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/access-lists', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/access-lists/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/access-lists/' + id); - } - }, - - Certificates: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/certificates', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - - const timeout = 180000 + (data && data.meta && data.meta.propagation_seconds ? Number(data.meta.propagation_seconds) * 1000 : 0); - return fetch('post', 'nginx/certificates', data, {timeout}); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/certificates/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/certificates/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - upload: function (id, form_data) { - return FileUpload('nginx/certificates/' + id + '/upload', form_data); - }, - - /** - * @param {FormData} form_data - * @params {Promise} - */ - validate: function (form_data) { - return FileUpload('nginx/certificates/validate', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - renew: function (id, timeout = 180000) { - return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout}); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - testHttpChallenge: function (domains) { - return fetch('get', 'nginx/certificates/test-http?' + new URLSearchParams({ - domains: JSON.stringify(domains), - })); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - download: function (id) { - return DownloadFile('get', "nginx/certificates/" + id + "/download", "certificate.zip") - } - } - }, - - AuditLog: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('audit-log', expand, query); - } - }, - - Reports: { - - /** - * @returns {Promise} - */ - getHostStats: function () { - return fetch('get', 'reports/hosts'); - } - }, - - Settings: { - - /** - * @param {String} setting_id - * @returns {Promise} - */ - getById: function (setting_id) { - return fetch('get', 'settings/' + setting_id); - }, - - /** - * @returns {Promise} - */ - getAll: function () { - return getAllObjects('settings'); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'settings/' + id, data); - } - } -}; diff --git a/frontend/js/app/audit-log/list/item.ejs b/frontend/js/app/audit-log/list/item.ejs deleted file mode 100644 index 84743c8d..00000000 --- a/frontend/js/app/audit-log/list/item.ejs +++ /dev/null @@ -1,80 +0,0 @@ - -
- -
- - -
- <% if (user.is_deleted) { - %> - <%- user.name %> - <% - } else { - %> - <%- user.name %> - <% - } - %> -
- - -
- <% - var items = []; - switch (object_type) { - case 'proxy-host': - %> <% - items = meta.domain_names; - break; - case 'redirection-host': - %> <% - items = meta.domain_names; - break; - case 'stream': - %> <% - items.push(meta.incoming_port); - break; - case 'dead-host': - %> <% - items = meta.domain_names; - break; - case 'access-list': - %> <% - items.push(meta.name); - break; - case 'user': - %> <% - items.push(meta.name); - break; - case 'certificate': - %> <% - if (meta.provider === 'letsencrypt') { - items = meta.domain_names; - } else { - items.push(meta.nice_name); - } - break; - } - %> <%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %> - — - <% - if (items && items.length) { - items.map(function(item) { - %> - <%- item %> - <% - }); - } else { - %> - #<%- object_id %> - <% - } - %> -
-
- <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %> -
- - - <%- i18n('audit-log', 'view-meta') %> - diff --git a/frontend/js/app/audit-log/list/item.js b/frontend/js/app/audit-log/list/item.js deleted file mode 100644 index 862ffc22..00000000 --- a/frontend/js/app/audit-log/list/item.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const Controller = require('../../controller'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - meta: 'a.meta' - }, - - events: { - 'click @ui.meta': function (e) { - e.preventDefault(); - Controller.showAuditMeta(this.model); - } - }, - - templateContext: { - more: function() { - switch (this.object_type) { - case 'redirection-host': - case 'stream': - case 'proxy-host': - return this.meta.domain_names.join(', '); - } - - return '#' + (this.object_id || '?'); - } - } -}); diff --git a/frontend/js/app/audit-log/list/main.ejs b/frontend/js/app/audit-log/list/main.ejs deleted file mode 100644 index ec3cf2a2..00000000 --- a/frontend/js/app/audit-log/list/main.ejs +++ /dev/null @@ -1,9 +0,0 @@ - -   - User - Event -   - - - - diff --git a/frontend/js/app/audit-log/list/main.js b/frontend/js/app/audit-log/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/audit-log/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/audit-log/main.ejs b/frontend/js/app/audit-log/main.ejs deleted file mode 100644 index 8d182b59..00000000 --- a/frontend/js/app/audit-log/main.ejs +++ /dev/null @@ -1,25 +0,0 @@ -
-
-
-

<%- i18n('audit-log', 'title') %>

-
- -
-
-
-
-
-
- -
-
- -
-
diff --git a/frontend/js/app/audit-log/main.js b/frontend/js/app/audit-log/main.js deleted file mode 100644 index 0d03c5ca..00000000 --- a/frontend/js/app/audit-log/main.js +++ /dev/null @@ -1,82 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const AuditLogModel = require('../../models/audit-log'); -const ListView = require('./list/main'); -const template = require('./main.ejs'); -const ErrorView = require('../error/main'); -const EmptyView = require('../empty/main'); - -module.exports = Mn.View.extend({ - id: 'audit-log', - template: template, - - ui: { - list_region: '.list-region', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.AuditLog.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new AuditLogModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showAuditLog(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - this.showChildView('list_region', new EmptyView({ - title: App.i18n('audit-log', 'empty'), - subtitle: App.i18n('audit-log', 'empty-subtitle') - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['user'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - onRender: function () { - let view = this; - - view.fetch(['user']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/audit-log/meta.ejs b/frontend/js/app/audit-log/meta.ejs deleted file mode 100644 index 98a2d973..00000000 --- a/frontend/js/app/audit-log/meta.ejs +++ /dev/null @@ -1,27 +0,0 @@ - diff --git a/frontend/js/app/audit-log/meta.js b/frontend/js/app/audit-log/meta.js deleted file mode 100644 index 815cdfac..00000000 --- a/frontend/js/app/audit-log/meta.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./meta.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog wide' -}); diff --git a/frontend/js/app/cache.js b/frontend/js/app/cache.js deleted file mode 100644 index 6d1fbc4f..00000000 --- a/frontend/js/app/cache.js +++ /dev/null @@ -1,10 +0,0 @@ -const UserModel = require('../models/user'); - -let cache = { - User: new UserModel.Model(), - locale: 'en', - version: null -}; - -module.exports = cache; - diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js deleted file mode 100644 index ccb2978a..00000000 --- a/frontend/js/app/controller.js +++ /dev/null @@ -1,447 +0,0 @@ -const Backbone = require('backbone'); -const Cache = require('./cache'); -const Tokens = require('./tokens'); - -module.exports = { - - /** - * @param {String} route - * @param {Object} [options] - * @returns {Boolean} - */ - navigate: function (route, options) { - options = options || {}; - Backbone.history.navigate(route.toString(), options); - return true; - }, - - /** - * Login - */ - showLogin: function () { - window.location = '/login'; - }, - - /** - * Users - */ - showUsers: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './users/main'], (App, View) => { - controller.navigate('/users'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * User Form - * - * @param [model] - */ - showUserForm: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './user/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Permissions Form - * - * @param model - */ - showUserPermissions: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './user/permissions'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Password Form - * - * @param model - */ - showUserPasswordForm: function (model) { - if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) { - require(['./main', './user/password'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Delete Confirm - * - * @param model - */ - showUserDeleteConfirm: function (model) { - if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { - require(['./main', './user/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Dashboard - */ - showDashboard: function () { - let controller = this; - - require(['./main', './dashboard/main'], (App, View) => { - controller.navigate('/'); - App.UI.showAppContent(new View()); - }); - }, - - /** - * Nginx Proxy Hosts - */ - showNginxProxy: function () { - if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) { - let controller = this; - - require(['./main', './nginx/proxy/main'], (App, View) => { - controller.navigate('/nginx/proxy'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Proxy Host Form - * - * @param [model] - */ - showNginxProxyForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { - require(['./main', './nginx/proxy/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Proxy Host Delete Confirm - * - * @param model - */ - showNginxProxyDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { - require(['./main', './nginx/proxy/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Redirection Hosts - */ - showNginxRedirection: function () { - if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { - let controller = this; - - require(['./main', './nginx/redirection/main'], (App, View) => { - controller.navigate('/nginx/redirection'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Redirection Host Form - * - * @param [model] - */ - showNginxRedirectionForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { - require(['./main', './nginx/redirection/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Proxy Redirection Delete Confirm - * - * @param model - */ - showNginxRedirectionDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { - require(['./main', './nginx/redirection/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Stream Hosts - */ - showNginxStream: function () { - if (Cache.User.isAdmin() || Cache.User.canView('streams')) { - let controller = this; - - require(['./main', './nginx/stream/main'], (App, View) => { - controller.navigate('/nginx/stream'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Stream Form - * - * @param [model] - */ - showNginxStreamForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { - require(['./main', './nginx/stream/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Stream Delete Confirm - * - * @param model - */ - showNginxStreamDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { - require(['./main', './nginx/stream/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Dead Hosts - */ - showNginxDead: function () { - if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { - let controller = this; - - require(['./main', './nginx/dead/main'], (App, View) => { - controller.navigate('/nginx/404'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Dead Host Form - * - * @param [model] - */ - showNginxDeadForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { - require(['./main', './nginx/dead/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Dead Host Delete Confirm - * - * @param model - */ - showNginxDeadDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { - require(['./main', './nginx/dead/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Help Dialog - * - * @param {String} title - * @param {String} content - */ - showHelp: function (title, content) { - require(['./main', './help/main'], function (App, View) { - App.UI.showModalDialog(new View({title: title, content: content})); - }); - }, - - /** - * Nginx Access - */ - showNginxAccess: function () { - if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { - let controller = this; - - require(['./main', './nginx/access/main'], (App, View) => { - controller.navigate('/nginx/access'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Access List Form - * - * @param [model] - */ - showNginxAccessListForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { - require(['./main', './nginx/access/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Access List Delete Confirm - * - * @param model - */ - showNginxAccessListDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { - require(['./main', './nginx/access/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Certificates - */ - showNginxCertificates: function () { - if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { - let controller = this; - - require(['./main', './nginx/certificates/main'], (App, View) => { - controller.navigate('/nginx/certificates'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Certificate Form - * - * @param [model] - */ - showNginxCertificateForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Certificate Renew - * - * @param model - */ - showNginxCertificateRenew: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/renew'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Certificate Delete Confirm - * - * @param model - */ - showNginxCertificateDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Certificate Test Reachability - * - * @param model - */ - showNginxCertificateTestReachability: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/test'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Audit Log - */ - showAuditLog: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './audit-log/main'], (App, View) => { - controller.navigate('/audit-log'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * Audit Log Metadata - * - * @param model - */ - showAuditMeta: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './audit-log/meta'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Settings - */ - showSettings: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './settings/main'], (App, View) => { - controller.navigate('/settings'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * Settings Item Form - * - * @param model - */ - showSettingForm: function (model) { - if (Cache.User.isAdmin()) { - if (model.get('id') === 'default-site') { - require(['./main', './settings/default-site/main'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - } - }, - - /** - * Logout - */ - logout: function () { - Tokens.dropTopToken(); - this.showLogin(); - } -}; diff --git a/frontend/js/app/dashboard/main.ejs b/frontend/js/app/dashboard/main.ejs deleted file mode 100644 index c00aa6d0..00000000 --- a/frontend/js/app/dashboard/main.ejs +++ /dev/null @@ -1,67 +0,0 @@ - - -<% if (columns) { %> -
- <% if (canShow('proxy_hosts')) { %> - - <% } %> - - <% if (canShow('redirection_hosts')) { %> - - <% } %> - - <% if (canShow('streams')) { %> - - <% } %> - - <% if (canShow('dead_hosts')) { %> - - <% } %> -
-<% } %> diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js deleted file mode 100644 index c2e82f85..00000000 --- a/frontend/js/app/dashboard/main.js +++ /dev/null @@ -1,92 +0,0 @@ -const Mn = require('backbone.marionette'); -const Cache = require('../cache'); -const Controller = require('../controller'); -const Api = require('../api'); -const Helpers = require('../../lib/helpers'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - id: 'dashboard', - columns: 0, - - stats: {}, - - ui: { - links: 'a' - }, - - events: { - 'click @ui.links': function (e) { - e.preventDefault(); - Controller.navigate($(e.currentTarget).attr('href'), true); - } - }, - - templateContext: function () { - let view = this; - - return { - getUserName: function () { - return Cache.User.get('nickname') || Cache.User.get('name'); - }, - - getHostStat: function (type) { - if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') { - return Helpers.niceNumber(view.stats.hosts[type]); - } - - return '-'; - }, - - canShow: function (perm) { - return Cache.User.isAdmin() || Cache.User.canView(perm); - }, - - columns: view.columns - }; - }, - - onRender: function () { - let view = this; - - if (typeof view.stats.hosts === 'undefined') { - Api.Reports.getHostStats() - .then(response => { - if (!view.isDestroyed()) { - view.stats.hosts = response; - view.render(); - } - }) - .catch(err => { - console.log(err); - }); - } - }, - - /** - * @param {Object} [model] - */ - preRender: function (model) { - this.columns = 0; - - // calculate the available columns based on permissions for the objects - // and store as a variable - //let view = this; - let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts']; - - perms.map(perm => { - this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0; - }); - - // Prevent double rendering on initial calls - if (typeof model !== 'undefined') { - this.render(); - } - }, - - initialize: function () { - this.preRender(); - this.listenTo(Cache.User, 'change', this.preRender); - } -}); diff --git a/frontend/js/app/empty/main.ejs b/frontend/js/app/empty/main.ejs deleted file mode 100644 index 11633dfc..00000000 --- a/frontend/js/app/empty/main.ejs +++ /dev/null @@ -1,11 +0,0 @@ -<% if (title) { %> -

<%- title %>

-<% } - -if (subtitle) { %> -

<%- subtitle %>

-<% } - -if (link) { %> - <%- link %> -<% } %> diff --git a/frontend/js/app/empty/main.js b/frontend/js/app/empty/main.js deleted file mode 100644 index 74998d65..00000000 --- a/frontend/js/app/empty/main.js +++ /dev/null @@ -1,33 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - className: 'text-center m-7', - template: template, - - options: { - btn_color: 'teal' - }, - - ui: { - action: 'a' - }, - - events: { - 'click @ui.action': function (e) { - e.preventDefault(); - this.getOption('action')(); - } - }, - - templateContext: function () { - return { - title: this.getOption('title'), - subtitle: this.getOption('subtitle'), - link: this.getOption('link'), - action: typeof this.getOption('action') === 'function', - btn_color: this.getOption('btn_color') - }; - } - -}); diff --git a/frontend/js/app/error/main.ejs b/frontend/js/app/error/main.ejs deleted file mode 100644 index f7fd709b..00000000 --- a/frontend/js/app/error/main.ejs +++ /dev/null @@ -1,7 +0,0 @@ - -<%= code ? '' + code + ' — ' : '' %> -<%- message %> - -<% if (retry) { %> -

<%- i18n('str', 'try-again') %> -<% } %> diff --git a/frontend/js/app/error/main.js b/frontend/js/app/error/main.js deleted file mode 100644 index 6fa85fc8..00000000 --- a/frontend/js/app/error/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'alert alert-icon alert-warning m-5', - - ui: { - retry: 'a.retry' - }, - - events: { - 'click @ui.retry': function (e) { - e.preventDefault(); - this.getOption('retry')(); - } - }, - - templateContext: function () { - return { - message: this.getOption('message'), - code: this.getOption('code'), - retry: typeof this.getOption('retry') === 'function' - }; - } - -}); diff --git a/frontend/js/app/help/main.ejs b/frontend/js/app/help/main.ejs deleted file mode 100644 index 6fb79e66..00000000 --- a/frontend/js/app/help/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/frontend/js/app/help/main.js b/frontend/js/app/help/main.js deleted file mode 100644 index b0f54374..00000000 --- a/frontend/js/app/help/main.js +++ /dev/null @@ -1,16 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog wide', - - templateContext: function () { - let content = this.getOption('content').split("\n"); - - return { - title: this.getOption('title'), - content: '

' + content.join('

') + '

' - }; - } -}); diff --git a/frontend/js/app/i18n.js b/frontend/js/app/i18n.js deleted file mode 100644 index c63cdc07..00000000 --- a/frontend/js/app/i18n.js +++ /dev/null @@ -1,23 +0,0 @@ -const Cache = ('./cache'); -const messages = require('../i18n/messages.json'); - -/** - * @param {String} namespace - * @param {String} key - * @param {Object} [data] - */ -module.exports = function (namespace, key, data) { - let locale = Cache.locale; - // check that the locale exists - if (typeof messages[locale] === 'undefined') { - locale = 'en'; - } - - if (typeof messages[locale][namespace] !== 'undefined' && typeof messages[locale][namespace][key] !== 'undefined') { - return messages[locale][namespace][key](data); - } else if (locale !== 'en' && typeof messages['en'][namespace] !== 'undefined' && typeof messages['en'][namespace][key] !== 'undefined') { - return messages['en'][namespace][key](data); - } - - return '(MISSING: ' + namespace + '/' + key + ')'; -}; diff --git a/frontend/js/app/main.js b/frontend/js/app/main.js deleted file mode 100644 index e85b4f62..00000000 --- a/frontend/js/app/main.js +++ /dev/null @@ -1,155 +0,0 @@ -const _ = require('underscore'); -const Backbone = require('backbone'); -const Mn = require('../lib/marionette'); -const Cache = require('./cache'); -const Controller = require('./controller'); -const Router = require('./router'); -const Api = require('./api'); -const Tokens = require('./tokens'); -const UI = require('./ui/main'); -const i18n = require('./i18n'); - -const App = Mn.Application.extend({ - - Cache: Cache, - Api: Api, - UI: null, - i18n: i18n, - Controller: Controller, - - region: { - el: '#app', - replaceElement: true - }, - - onStart: function (app, options) { - console.log(i18n('main', 'welcome')); - - // Check if token is coming through - if (this.getParam('token')) { - Tokens.addToken(this.getParam('token')); - } - - // Check if we are still logged in by refreshing the token - Api.status() - .then(result => { - Cache.version = [result.version.major, result.version.minor, result.version.revision].join('.'); - }) - .then(Api.Tokens.refresh) - .then(this.bootstrap) - .then(() => { - console.info(i18n('main', 'logged-in', Cache.User.attributes)); - this.bootstrapTimer(); - this.refreshTokenTimer(); - - this.UI = new UI(); - this.UI.on('render', () => { - new Router(options); - Backbone.history.start({pushState: true}); - - // Ask the admin use to change their details - if (Cache.User.get('email') === 'admin@example.com') { - Controller.showUserForm(Cache.User); - } - }); - - this.getRegion().show(this.UI); - }) - .catch(err => { - console.warn('Not logged in:', err.message); - Controller.showLogin(); - }); - }, - - History: { - replace: function (data) { - window.history.replaceState(_.extend(window.history.state || {}, data), document.title); - }, - - get: function (attr) { - return window.history.state ? window.history.state[attr] : undefined; - } - }, - - getParam: function (name) { - name = name.replace(/[\[\]]/g, '\\$&'); - let url = window.location.href; - let regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); - let results = regex.exec(url); - - if (!results) { - return null; - } - - if (!results[2]) { - return ''; - } - - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }, - - /** - * Get user and other base info to start prime the cache and the application - * - * @returns {Promise} - */ - bootstrap: function () { - return Api.Users.getById('me', ['permissions']) - .then(response => { - Cache.User.set(response); - Tokens.setCurrentName(response.nickname || response.name); - }); - }, - - /** - * Bootstraps the user from time to time - */ - bootstrapTimer: function () { - setTimeout(() => { - Api.status() - .then(result => { - let version = [result.version.major, result.version.minor, result.version.revision].join('.'); - if (version !== Cache.version) { - document.location.reload(); - } - }) - .then(this.bootstrap) - .then(() => { - this.bootstrapTimer(); - }) - .catch(err => { - if (err.message !== 'timeout' && err.code && err.code !== 400) { - console.log(err); - console.error(err.message); - console.info('Not logged in?'); - Controller.showLogin(); - } else { - this.bootstrapTimer(); - } - }); - }, 30 * 1000); // 30 seconds - }, - - refreshTokenTimer: function () { - setTimeout(() => { - return Api.Tokens.refresh() - .then(this.bootstrap) - .then(() => { - this.refreshTokenTimer(); - }) - .catch(err => { - if (err.message !== 'timeout' && err.code && err.code !== 400) { - console.log(err); - console.error(err.message); - console.info('Not logged in?'); - Controller.showLogin(); - } else { - this.refreshTokenTimer(); - } - }); - }, 10 * 60 * 1000); - } -}); - -const app = new App(); -module.exports = app; diff --git a/frontend/js/app/nginx/access/delete.ejs b/frontend/js/app/nginx/access/delete.ejs deleted file mode 100644 index 3833549a..00000000 --- a/frontend/js/app/nginx/access/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/access/delete.js b/frontend/js/app/nginx/access/delete.js deleted file mode 100644 index 4af91ab1..00000000 --- a/frontend/js/app/nginx/access/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.AccessLists.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxAccess(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/access/form.ejs b/frontend/js/app/nginx/access/form.ejs deleted file mode 100644 index 79220b14..00000000 --- a/frontend/js/app/nginx/access/form.ejs +++ /dev/null @@ -1,108 +0,0 @@ - diff --git a/frontend/js/app/nginx/access/form.js b/frontend/js/app/nginx/access/form.js deleted file mode 100644 index bb075548..00000000 --- a/frontend/js/app/nginx/access/form.js +++ /dev/null @@ -1,153 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const AccessListModel = require('../../../models/access-list'); -const template = require('./form.ejs'); -const ItemView = require('./form/item'); -const ClientView = require('./form/client'); - -require('jquery-serializejson'); - -const ItemsView = Mn.CollectionView.extend({ - childView: ItemView -}); - -const ClientsView = Mn.CollectionView.extend({ - childView: ClientView -}); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - items_region: '.items', - clients_region: '.clients', - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - access_add: 'button.access_add', - auth_add: 'button.auth_add' - }, - - regions: { - items_region: '@ui.items_region', - clients_region: '@ui.clients_region' - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let form_data = this.ui.form.serializeJSON(); - let items_data = []; - let clients_data = []; - - form_data.username.map(function (val, idx) { - if (val.trim().length) { - items_data.push({ - username: val.trim(), - password: form_data.password[idx] - }); - } - }); - - form_data.address.map(function (val, idx) { - if (val.trim().length) { - clients_data.push({ - address: val.trim(), - directive: form_data.directive[idx] - }) - } - }); - - if (!items_data.length && !clients_data.length) { - alert('You must specify at least 1 Authorization or Access rule'); - return; - } - - let data = { - name: form_data.name, - satisfy_any: !!form_data.satisfy_any, - pass_auth: !!form_data.pass_auth, - items: items_data, - clients: clients_data - }; - - console.log(data); - - let method = App.Api.Nginx.AccessLists.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.AccessLists.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxAccess(); - } - }); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - }, - 'click @ui.access_add': function (e) { - e.preventDefault(); - - let clients = this.model.get('clients'); - clients.push({}); - this.showChildView('clients_region', new ClientsView({ - collection: new Backbone.Collection(clients) - })); - }, - 'click @ui.auth_add': function (e) { - e.preventDefault(); - - let items = this.model.get('items'); - items.push({}); - this.showChildView('items_region', new ItemsView({ - collection: new Backbone.Collection(items) - })); - } - }, - - onRender: function () { - let items = this.model.get('items'); - let clients = this.model.get('clients'); - - // Ensure at least one field is shown initally - if (!items.length) items.push({}); - if (!clients.length) clients.push({}); - - this.showChildView('items_region', new ItemsView({ - collection: new Backbone.Collection(items) - })); - - this.showChildView('clients_region', new ClientsView({ - collection: new Backbone.Collection(clients) - })); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new AccessListModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/access/form/client.ejs b/frontend/js/app/nginx/access/form/client.ejs deleted file mode 100644 index 6b767b83..00000000 --- a/frontend/js/app/nginx/access/form/client.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
-
- -
-
-
-
- -
-
diff --git a/frontend/js/app/nginx/access/form/client.js b/frontend/js/app/nginx/access/form/client.js deleted file mode 100644 index b4c00e2e..00000000 --- a/frontend/js/app/nginx/access/form/client.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./client.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'row' -}); diff --git a/frontend/js/app/nginx/access/form/item.ejs b/frontend/js/app/nginx/access/form/item.ejs deleted file mode 100644 index c2435ecb..00000000 --- a/frontend/js/app/nginx/access/form/item.ejs +++ /dev/null @@ -1,10 +0,0 @@ -
-
- -
-
-
-
- -
-
diff --git a/frontend/js/app/nginx/access/form/item.js b/frontend/js/app/nginx/access/form/item.js deleted file mode 100644 index f15238dc..00000000 --- a/frontend/js/app/nginx/access/form/item.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'row' -}); diff --git a/frontend/js/app/nginx/access/list/item.ejs b/frontend/js/app/nginx/access/list/item.ejs deleted file mode 100644 index 2ee37a50..00000000 --- a/frontend/js/app/nginx/access/list/item.ejs +++ /dev/null @@ -1,42 +0,0 @@ - -
- -
- - -
- <%- name %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - - <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> - - - <%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %> - - - <% if (satisfy_any) { %> - <%- i18n('str', 'any') %> - <%} else { %> - <%- i18n('str', 'all') %> - <% } %> - - - <%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %> - -<% if (canManage) { %> - - - -<% } %> diff --git a/frontend/js/app/nginx/access/list/item.js b/frontend/js/app/nginx/access/list/item.js deleted file mode 100644 index 4f68aead..00000000 --- a/frontend/js/app/nginx/access/list/item.js +++ /dev/null @@ -1,33 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit', - delete: 'a.delete' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListDeleteConfirm(this.model); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('access_lists') - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/access/list/main.ejs b/frontend/js/app/nginx/access/list/main.ejs deleted file mode 100644 index 7988e0c2..00000000 --- a/frontend/js/app/nginx/access/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('access-lists', 'authorization') %> - <%- i18n('access-lists', 'access') %> - <%- i18n('access-lists', 'satisfy') %> - <%- i18n('proxy-hosts', 'title') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/access/list/main.js b/frontend/js/app/nginx/access/list/main.js deleted file mode 100644 index 577a77ef..00000000 --- a/frontend/js/app/nginx/access/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('access_lists') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/access/main.ejs b/frontend/js/app/nginx/access/main.ejs deleted file mode 100644 index 97585936..00000000 --- a/frontend/js/app/nginx/access/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('access-lists', 'title') %>

-
- - - <% if (showAddButton) { %> - <%- i18n('access-lists', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/access/main.js b/frontend/js/app/nginx/access/main.js deleted file mode 100644 index 513f5865..00000000 --- a/frontend/js/app/nginx/access/main.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const AccessListModel = require('../../../models/access-list'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-access', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.AccessLists.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new AccessListModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxAccess(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('access_lists'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('access-lists', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('access-lists', 'add') : null, - btn_color: 'teal', - permission: 'access_lists', - action: function () { - App.Controller.showNginxAccessListForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('access-lists', 'help-title'), App.i18n('access-lists', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'items', 'clients'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('access_lists') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'items', 'clients']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/certificates-list-item.ejs b/frontend/js/app/nginx/certificates-list-item.ejs deleted file mode 100644 index aa4b53ad..00000000 --- a/frontend/js/app/nginx/certificates-list-item.ejs +++ /dev/null @@ -1,18 +0,0 @@ -
- <% if (id === 'new') { %> -
- <%- i18n('all-hosts', 'new-cert') %> -
- <%- i18n('all-hosts', 'with-le') %> - <% } else if (id > 0) { %> -
- <%- provider === 'other' ? nice_name : domain_names.join(', ') %> -
- <%- i18n('ssl', provider) %> – Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> - <% } else { %> -
- <%- i18n('all-hosts', 'none') %> -
- <%- i18n('all-hosts', 'no-ssl') %> - <% } %> -
diff --git a/frontend/js/app/nginx/certificates/delete.ejs b/frontend/js/app/nginx/certificates/delete.ejs deleted file mode 100644 index b4e06866..00000000 --- a/frontend/js/app/nginx/certificates/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/delete.js b/frontend/js/app/nginx/certificates/delete.js deleted file mode 100644 index 89a2e5e8..00000000 --- a/frontend/js/app/nginx/certificates/delete.js +++ /dev/null @@ -1,34 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.save.addClass('btn-loading'); - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Nginx.Certificates.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxCertificates(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs deleted file mode 100644 index 7fc12785..00000000 --- a/frontend/js/app/nginx/certificates/form.ejs +++ /dev/null @@ -1,185 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js deleted file mode 100644 index a56c3f8e..00000000 --- a/frontend/js/app/nginx/certificates/form.js +++ /dev/null @@ -1,294 +0,0 @@ -const _ = require('underscore'); -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const CertificateModel = require('../../../models/certificate'); -const template = require('./form.ejs'); -const i18n = require('../../i18n'); -const dns_providers = sortProvidersAlphabetically(require('../../../../../global/certbot-dns-plugins')); - -require('jquery-serializejson'); -require('selectize'); - -function sortProvidersAlphabetically(obj) { - return Object.entries(obj) - .sort((a,b) => a[1].display_name.toLowerCase() > b[1].display_name.toLowerCase()) - .reduce((result, entry) => { - result[entry[0]] = entry[1]; - return result; - }, {}); -} - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - max_file_size: 102400, - - ui: { - form: 'form', - loader_content: '.loader-content', - non_loader_content: '.non-loader-content', - le_error_info: '#le-error-info', - domain_names: 'input[name="domain_names"]', - test_domains_container: '.test-domains-container', - test_domains_button: '.test-domains', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - other_certificate: '#other_certificate', - other_certificate_label: '#other_certificate_label', - other_certificate_key: '#other_certificate_key', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - other_certificate_key_label: '#other_certificate_key_label', - other_intermediate_certificate: '#other_intermediate_certificate', - other_intermediate_certificate_label: '#other_intermediate_certificate_label' - }, - - events: { - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - this.ui.test_domains_container.hide(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - this.ui.test_domains_container.show(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - $(this).removeClass('btn-loading'); - return; - } - - let data = this.ui.form.serializeJSON(); - data.provider = this.model.get('provider'); - let ssl_files = []; - - if (data.provider === 'letsencrypt') { - if (typeof data.meta === 'undefined') data.meta = {}; - - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.split(',').map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - - // Manipulate - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - } else if (data.provider === 'other' && !this.model.hasSslFiles()) { - // check files are attached - if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) { - alert('Certificate file is not attached'); - return; - } else { - if (this.ui.other_certificate[0].files[0].size > this.max_file_size) { - alert('Certificate file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]}); - } - - if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) { - alert('Certificate key file is not attached'); - return; - } else { - if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) { - alert('Certificate key file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]}); - } - - if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) { - if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) { - alert('Intermediate Certificate file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]}); - } - } - - this.ui.loader_content.show(); - this.ui.non_loader_content.hide(); - - // compile file data - let form_data = new FormData(); - if (data.provider === 'other' && ssl_files.length) { - ssl_files.map(function (file) { - form_data.append(file.name, file.file); - }); - } - - new Promise(resolve => { - if (data.provider === 'other') { - resolve(App.Api.Nginx.Certificates.validate(form_data)); - } else { - resolve(); - } - }) - .then(() => { - return App.Api.Nginx.Certificates.create(data); - }) - .then(result => { - this.model.set(result); - - // Now upload the certs if we need to - if (data.provider === 'other') { - return App.Api.Nginx.Certificates.upload(this.model.get('id'), form_data) - .then(result => { - this.model.set('meta', _.assign({}, this.model.get('meta'), result)); - }); - } - }) - .then(() => { - App.UI.closeModal(function () { - App.Controller.showNginxCertificates(); - }); - }) - .catch(err => { - let more_info = ''; - if (err.code === 500 && err.debug) { - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.loader_content.hide(); - this.ui.non_loader_content.show(); - }); - }, - 'click @ui.test_domains_button': function (e) { - e.preventDefault(); - const domainNames = this.ui.domain_names[0].value.split(','); - if (domainNames && domainNames.length > 0) { - this.model.set('domain_names', domainNames); - this.model.set('back_to_add', true); - App.Controller.showNginxCertificateTestReachability(this.model); - } - }, - 'change @ui.domain_names': function(e){ - const domainNames = e.target.value.split(','); - if (domainNames && domainNames.length > 0) { - this.ui.test_domains_button.prop('disabled', false); - } else { - this.ui.test_domains_button.prop('disabled', true); - } - }, - 'change @ui.other_certificate_key': function(e){ - this.setFileName("other_certificate_key_label", e) - }, - 'change @ui.other_certificate': function(e){ - this.setFileName("other_certificate_label", e) - }, - 'change @ui.other_intermediate_certificate': function(e){ - this.setFileName("other_intermediate_certificate_label", e) - } - }, - setFileName(ui, e){ - this.getUI(ui).text(e.target.files[0].name) - }, - templateContext: { - getLetsencryptEmail: function () { - return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); - }, - getLetsencryptAgree: function () { - return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.loader_content.hide(); - this.ui.le_error_info.hide(); - if (this.ui.domain_names[0]) { - const domainNames = this.ui.domain_names[0].value.split(','); - if (!domainNames || domainNames.length === 0 || (domainNames.length === 1 && domainNames[0] === "")) { - this.ui.test_domains_button.prop('disabled', true); - } - } - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new CertificateModel.Model({provider: 'letsencrypt'}); - } - } -}); diff --git a/frontend/js/app/nginx/certificates/list/item.ejs b/frontend/js/app/nginx/certificates/list/item.ejs deleted file mode 100644 index bdeeecad..00000000 --- a/frontend/js/app/nginx/certificates/list/item.ejs +++ /dev/null @@ -1,54 +0,0 @@ - -
- -
- - -
- <% - if (provider === 'letsencrypt') { - domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - } else { - %><%- nice_name %><% - } - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - - <%- i18n('ssl', provider) %><% if (meta.dns_provider) { %> - <%- dns_providers[meta.dns_provider].display_name %><% } %> - - - <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/certificates/list/item.js b/frontend/js/app/nginx/certificates/list/item.js deleted file mode 100644 index 7fa1c681..00000000 --- a/frontend/js/app/nginx/certificates/list/item.js +++ /dev/null @@ -1,58 +0,0 @@ -const Mn = require('backbone.marionette'); -const moment = require('moment'); -const App = require('../../../main'); -const template = require('./item.ejs'); -const dns_providers = require('../../../../../../global/certbot-dns-plugins'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - host_link: '.host-link', - renew: 'a.renew', - delete: 'a.delete', - download: 'a.download', - test: 'a.test' - }, - - events: { - 'click @ui.renew': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateRenew(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - }, - - 'click @ui.download': function (e) { - e.preventDefault(); - App.Api.Nginx.Certificates.download(this.model.get('id')); - }, - - 'click @ui.test': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateTestReachability(this.model); - }, - }, - - templateContext: { - canManage: App.Cache.User.canManage('certificates'), - isExpired: function () { - return moment(this.expires_on).isBefore(moment()); - }, - dns_providers: dns_providers - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/certificates/list/main.ejs b/frontend/js/app/nginx/certificates/list/main.ejs deleted file mode 100644 index aa49a27f..00000000 --- a/frontend/js/app/nginx/certificates/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('all-hosts', 'cert-provider') %> - <%- i18n('str', 'expires') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/certificates/list/main.js b/frontend/js/app/nginx/certificates/list/main.js deleted file mode 100644 index d96b43e8..00000000 --- a/frontend/js/app/nginx/certificates/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('certificates') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/certificates/main.ejs b/frontend/js/app/nginx/certificates/main.ejs deleted file mode 100644 index dbd6fa85..00000000 --- a/frontend/js/app/nginx/certificates/main.ejs +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
-

<%- i18n('certificates', 'title') %>

-
- - - <% if (showAddButton) { %> - - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/certificates/main.js b/frontend/js/app/nginx/certificates/main.js deleted file mode 100644 index 89562768..00000000 --- a/frontend/js/app/nginx/certificates/main.js +++ /dev/null @@ -1,109 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const CertificateModel = require('../../../models/certificate'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-certificates', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.Certificates.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new CertificateModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxCertificates(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('certificates'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('certificates', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('certificates', 'add') : null, - btn_color: 'pink', - permission: 'certificates', - action: function () { - App.Controller.showNginxCertificateForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - let model = new CertificateModel.Model({provider: $(e.currentTarget).data('cert')}); - App.Controller.showNginxCertificateForm(model); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('certificates', 'help-title'), App.i18n('certificates', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('certificates') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/certificates/renew.ejs b/frontend/js/app/nginx/certificates/renew.ejs deleted file mode 100644 index 4af186d0..00000000 --- a/frontend/js/app/nginx/certificates/renew.ejs +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/renew.js b/frontend/js/app/nginx/certificates/renew.js deleted file mode 100644 index 73632881..00000000 --- a/frontend/js/app/nginx/certificates/renew.js +++ /dev/null @@ -1,31 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./renew.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - waiting: '.waiting', - error: '.error', - close: 'button.cancel' - }, - - onRender: function () { - this.ui.error.hide(); - - App.Api.Nginx.Certificates.renew(this.model.get('id')) - .then((result) => { - this.model.set(result); - setTimeout(() => { - App.UI.closeModal(); - }, 1000); - }) - .catch((err) => { - this.ui.waiting.hide(); - this.ui.error.text(err.message).show(); - this.ui.close.prop('disabled', false); - }); - } -}); diff --git a/frontend/js/app/nginx/certificates/test.ejs b/frontend/js/app/nginx/certificates/test.ejs deleted file mode 100644 index 6661f625..00000000 --- a/frontend/js/app/nginx/certificates/test.ejs +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/test.js b/frontend/js/app/nginx/certificates/test.js deleted file mode 100644 index 0886d26f..00000000 --- a/frontend/js/app/nginx/certificates/test.js +++ /dev/null @@ -1,75 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./test.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - waiting: '.waiting', - error: '.error', - success: '.success', - close: 'button.cancel' - }, - - events: { - 'click @ui.close': function (e) { - e.preventDefault(); - if (this.model.get('back_to_add')) { - App.Controller.showNginxCertificateForm(this.model); - } else { - App.UI.closeModal(); - } - }, - }, - - onRender: function () { - this.ui.error.hide(); - this.ui.success.hide(); - - App.Api.Nginx.Certificates.testHttpChallenge(this.model.get('domain_names')) - .then((result) => { - let allOk = true; - let text = ''; - - for (const domain in result) { - const status = result[domain]; - if (status === 'ok') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-ok')}

`; - } else { - allOk = false; - if (status === 'no-host') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-not-resolved')}

`; - } else if (status === 'failed') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-failed-to-check')}

`; - } else if (status === '404') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-404')}

`; - } else if (status === 'wrong-data') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-wrong-data')}

`; - } else if (status.startsWith('other:')) { - const code = status.substring(6); - text += `

${domain}: ${App.i18n('certificates', 'reachability-other', {code})}

`; - } else { - // This should never happen - text += `

${domain}: ?

`; - } - } - } - - this.ui.waiting.hide(); - if (allOk) { - this.ui.success.html(text).show(); - } else { - this.ui.error.html(text).show(); - } - this.ui.close.prop('disabled', false); - }) - .catch((e) => { - console.error(e); - this.ui.waiting.hide(); - this.ui.error.text(App.i18n('certificates', 'reachability-failed-to-reach-api')).show(); - this.ui.close.prop('disabled', false); - }); - } -}); diff --git a/frontend/js/app/nginx/dead/delete.ejs b/frontend/js/app/nginx/dead/delete.ejs deleted file mode 100644 index 4bebb436..00000000 --- a/frontend/js/app/nginx/dead/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/dead/delete.js b/frontend/js/app/nginx/dead/delete.js deleted file mode 100644 index d497d068..00000000 --- a/frontend/js/app/nginx/dead/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.DeadHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxDead(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/dead/form.ejs b/frontend/js/app/nginx/dead/form.ejs deleted file mode 100644 index 253c4b6f..00000000 --- a/frontend/js/app/nginx/dead/form.ejs +++ /dev/null @@ -1,206 +0,0 @@ - diff --git a/frontend/js/app/nginx/dead/form.js b/frontend/js/app/nginx/dead/form.js deleted file mode 100644 index 8f6774f6..00000000 --- a/frontend/js/app/nginx/dead/form.js +++ /dev/null @@ -1,286 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const DeadHostModel = require('../../../models/dead-host'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - letsencrypt: '.letsencrypt' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.http2_support = !!data.http2_support; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.DeadHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.DeadHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxDead(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new DeadHostModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/dead/list/item.ejs b/frontend/js/app/nginx/dead/list/item.ejs deleted file mode 100644 index d447bd1e..00000000 --- a/frontend/js/app/nginx/dead/list/item.ejs +++ /dev/null @@ -1,54 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/dead/list/item.js b/frontend/js/app/nginx/dead/list/item.js deleted file mode 100644 index a477dbfa..00000000 --- a/frontend/js/app/nginx/dead/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.DeadHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.DeadHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('dead_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/dead/list/main.ejs b/frontend/js/app/nginx/dead/list/main.ejs deleted file mode 100644 index e018a74b..00000000 --- a/frontend/js/app/nginx/dead/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/dead/list/main.js b/frontend/js/app/nginx/dead/list/main.js deleted file mode 100644 index 57931419..00000000 --- a/frontend/js/app/nginx/dead/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('dead_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/dead/main.ejs b/frontend/js/app/nginx/dead/main.ejs deleted file mode 100644 index 4c5d1ad1..00000000 --- a/frontend/js/app/nginx/dead/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('dead-hosts', 'title') %>

-
- - - <% if (showAddButton) { %> - <%- i18n('dead-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/dead/main.js b/frontend/js/app/nginx/dead/main.js deleted file mode 100644 index e4d0c010..00000000 --- a/frontend/js/app/nginx/dead/main.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const DeadHostModel = require('../../../models/dead-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-dead', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.DeadHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new DeadHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxDead(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('dead_hosts'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('dead-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('dead-hosts', 'add') : null, - btn_color: 'danger', - permission: 'dead_hosts', - action: function () { - App.Controller.showNginxDeadForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('dead-hosts', 'help-title'), App.i18n('dead-hosts', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('dead_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/proxy/access-list-item.ejs b/frontend/js/app/nginx/proxy/access-list-item.ejs deleted file mode 100644 index e5a7e116..00000000 --- a/frontend/js/app/nginx/proxy/access-list-item.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
- <% if (id > 0) { %> -
- <%- name %> -
- <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>, <%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %> – Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %> - <% } else { %> -
- <%- i18n('access-lists', 'public') %> -
- <%- i18n('access-lists', 'public-sub') %> - <% } %> -
diff --git a/frontend/js/app/nginx/proxy/delete.ejs b/frontend/js/app/nginx/proxy/delete.ejs deleted file mode 100644 index 74da297c..00000000 --- a/frontend/js/app/nginx/proxy/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/proxy/delete.js b/frontend/js/app/nginx/proxy/delete.js deleted file mode 100644 index 63a8e020..00000000 --- a/frontend/js/app/nginx/proxy/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.ProxyHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxProxy(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs deleted file mode 100644 index 56868f55..00000000 --- a/frontend/js/app/nginx/proxy/form.ejs +++ /dev/null @@ -1,281 +0,0 @@ - diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js deleted file mode 100644 index 1dfb5c18..00000000 --- a/frontend/js/app/nginx/proxy/form.js +++ /dev/null @@ -1,369 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const ProxyHostModel = require('../../../models/proxy-host'); -const ProxyLocationModel = require('../../../models/proxy-host-location'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const accessListItemTemplate = require('./access-list-item.ejs'); -const CustomLocation = require('./location'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - locationsCollection: new ProxyLocationModel.Collection(), - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - forward_host: 'input[name="forward_host"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - add_location_btn: 'button.add_location', - locations_container: '.locations_container', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - access_list_select: 'select[name="access_list_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - forward_scheme: 'select[name="forward_scheme"]', - letsencrypt: '.letsencrypt' - }, - - regions: { - locations_regions: '@ui.locations_container' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.add_location_btn': function (e) { - e.preventDefault(); - - const model = new ProxyLocationModel.Model(); - this.locationsCollection.add(model); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Add locations - data.locations = []; - this.locationsCollection.models.forEach((location) => { - data.locations.push(location.toJSON()); - }); - - // Serialize collects path from custom locations - // This field must be removed from root object - delete data.path; - - // Manipulate - data.forward_port = parseInt(data.forward_port, 10); - data.block_exploits = !!data.block_exploits; - data.caching_enabled = !!data.caching_enabled; - data.allow_websocket_upgrade = !!data.allow_websocket_upgrade; - data.http2_support = !!data.http2_support; - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.ProxyHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.ProxyHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxProxy(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - this.ui.ssl_forced.trigger('change'); - this.ui.hsts_enabled.trigger('change'); - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Access Lists - this.ui.access_list_select.selectize({ - valueField: 'id', - labelField: 'name', - searchField: ['name'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return accessListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.AccessLists.getAll(['items', 'clients']) - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id')); - } - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new ProxyHostModel.Model(); - } - - this.locationsCollection = new ProxyLocationModel.Collection(); - - // Custom locations - this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({ - collection: this.locationsCollection - })); - - // Check wether there are any location defined - if (options.model && Array.isArray(options.model.attributes.locations)) { - options.model.attributes.locations.forEach((location) => { - let m = new ProxyLocationModel.Model(location); - this.locationsCollection.add(m); - }); - } - } -}); diff --git a/frontend/js/app/nginx/proxy/list/item.ejs b/frontend/js/app/nginx/proxy/list/item.ejs deleted file mode 100644 index a5936804..00000000 --- a/frontend/js/app/nginx/proxy/list/item.ejs +++ /dev/null @@ -1,60 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forward_scheme %>://<%- forward_host %>:<%- forward_port %>
- - -
<%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - -
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/proxy/list/item.js b/frontend/js/app/nginx/proxy/list/item.js deleted file mode 100644 index 37d199b4..00000000 --- a/frontend/js/app/nginx/proxy/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.ProxyHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.ProxyHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/proxy/list/main.ejs b/frontend/js/app/nginx/proxy/list/main.ejs deleted file mode 100644 index 6de5b9c6..00000000 --- a/frontend/js/app/nginx/proxy/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('str', 'destination') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'access') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/proxy/list/main.js b/frontend/js/app/nginx/proxy/list/main.js deleted file mode 100644 index 09e984e6..00000000 --- a/frontend/js/app/nginx/proxy/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/proxy/location-item.ejs b/frontend/js/app/nginx/proxy/location-item.ejs deleted file mode 100644 index 39445f7b..00000000 --- a/frontend/js/app/nginx/proxy/location-item.ejs +++ /dev/null @@ -1,64 +0,0 @@ -
-
-
-
-
- -
-
-
- - location - - -
-
-
-
- -
-
-
-
-
-
-
- - -
-
-
-
- - - <%- i18n('proxy-hosts', 'custom-forward-host-help') %> -
-
-
-
- - -
-
-
-
-
-
- -
-
-
- - - <%- i18n('locations', 'delete') %> - -
-
diff --git a/frontend/js/app/nginx/proxy/location.js b/frontend/js/app/nginx/proxy/location.js deleted file mode 100644 index e9513a48..00000000 --- a/frontend/js/app/nginx/proxy/location.js +++ /dev/null @@ -1,54 +0,0 @@ -const locationItemTemplate = require('./location-item.ejs'); -const Mn = require('backbone.marionette'); -const App = require('../../main'); - -const LocationView = Mn.View.extend({ - template: locationItemTemplate, - className: 'location_block', - - ui: { - toggle: 'input[type="checkbox"]', - config: '.config', - delete: '.location-delete' - }, - - events: { - 'change @ui.toggle': function(el) { - if (el.target.checked) { - this.ui.config.show(); - } else { - this.ui.config.hide(); - } - }, - - 'change .model': function (e) { - const map = {}; - map[e.target.name] = e.target.value; - this.model.set(map); - }, - - 'click @ui.delete': function () { - this.model.destroy(); - } - }, - - onRender: function() { - $(this.ui.config).hide(); - }, - - templateContext: function() { - return { - i18n: App.i18n - } - } -}); - -const LocationCollectionView = Mn.CollectionView.extend({ - className: 'locations_container', - childView: LocationView -}); - -module.exports = { - LocationCollectionView, - LocationView -} \ No newline at end of file diff --git a/frontend/js/app/nginx/proxy/main.ejs b/frontend/js/app/nginx/proxy/main.ejs deleted file mode 100644 index 4ecb9036..00000000 --- a/frontend/js/app/nginx/proxy/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('proxy-hosts', 'title') %>

-
- - - <% if (showAddButton) { %> - <%- i18n('proxy-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/proxy/main.js b/frontend/js/app/nginx/proxy/main.js deleted file mode 100644 index baf67101..00000000 --- a/frontend/js/app/nginx/proxy/main.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const ProxyHostModel = require('../../../models/proxy-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-proxy', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.ProxyHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new ProxyHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxProxy(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('proxy_hosts'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('proxy-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('proxy-hosts', 'add') : null, - btn_color: 'success', - permission: 'proxy_hosts', - action: function () { - App.Controller.showNginxProxyForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('proxy-hosts', 'help-title'), App.i18n('proxy-hosts', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'access_list', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'access_list', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/redirection/delete.ejs b/frontend/js/app/nginx/redirection/delete.ejs deleted file mode 100644 index 782d8435..00000000 --- a/frontend/js/app/nginx/redirection/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/redirection/delete.js b/frontend/js/app/nginx/redirection/delete.js deleted file mode 100644 index 6d2862f6..00000000 --- a/frontend/js/app/nginx/redirection/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.RedirectionHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxRedirection(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/redirection/form.ejs b/frontend/js/app/nginx/redirection/form.ejs deleted file mode 100644 index 7e190719..00000000 --- a/frontend/js/app/nginx/redirection/form.ejs +++ /dev/null @@ -1,253 +0,0 @@ - diff --git a/frontend/js/app/nginx/redirection/form.js b/frontend/js/app/nginx/redirection/form.js deleted file mode 100644 index 1f81feeb..00000000 --- a/frontend/js/app/nginx/redirection/form.js +++ /dev/null @@ -1,288 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const RedirectionHostModel = require('../../../models/redirection-host'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - letsencrypt: '.letsencrypt' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - data.block_exploits = !!data.block_exploits; - data.preserve_path = !!data.preserve_path; - data.http2_support = !!data.http2_support; - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.RedirectionHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.RedirectionHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxRedirection(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new RedirectionHostModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/redirection/list/item.ejs b/frontend/js/app/nginx/redirection/list/item.ejs deleted file mode 100644 index 4f25d973..00000000 --- a/frontend/js/app/nginx/redirection/list/item.ejs +++ /dev/null @@ -1,63 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forward_http_code %>
- - -
<%- forward_scheme == '$scheme' ? 'auto' : forward_scheme %>
- - -
<%- forward_domain_name %>
- - -
<%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> diff --git a/frontend/js/app/nginx/redirection/list/item.js b/frontend/js/app/nginx/redirection/list/item.js deleted file mode 100644 index 05adc251..00000000 --- a/frontend/js/app/nginx/redirection/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.RedirectionHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.RedirectionHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('redirection_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/redirection/list/main.ejs b/frontend/js/app/nginx/redirection/list/main.ejs deleted file mode 100644 index 8b6930d6..00000000 --- a/frontend/js/app/nginx/redirection/list/main.ejs +++ /dev/null @@ -1,15 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('redirection-hosts', 'forward-http-status-code') %> - <%- i18n('redirection-hosts', 'forward-scheme') %> - <%- i18n('str', 'destination') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/redirection/list/main.js b/frontend/js/app/nginx/redirection/list/main.js deleted file mode 100644 index d368cf6a..00000000 --- a/frontend/js/app/nginx/redirection/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('redirection_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/redirection/main.ejs b/frontend/js/app/nginx/redirection/main.ejs deleted file mode 100644 index 87e28229..00000000 --- a/frontend/js/app/nginx/redirection/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('redirection-hosts', 'title') %>

-
- - - <% if (showAddButton) { %> - <%- i18n('redirection-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/redirection/main.js b/frontend/js/app/nginx/redirection/main.js deleted file mode 100644 index 1f5351a7..00000000 --- a/frontend/js/app/nginx/redirection/main.js +++ /dev/null @@ -1,107 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const RedirectionHostModel = require('../../../models/redirection-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-redirection', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.RedirectionHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new RedirectionHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxRedirection(); - } - })); - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('redirection_hosts'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('redirection-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('redirection-hosts', 'add') : null, - btn_color: 'yellow', - permission: 'redirection_hosts', - action: function () { - App.Controller.showNginxRedirectionForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('redirection-hosts', 'help-title'), App.i18n('redirection-hosts', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/stream/delete.ejs b/frontend/js/app/nginx/stream/delete.ejs deleted file mode 100644 index d7ba3a21..00000000 --- a/frontend/js/app/nginx/stream/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/nginx/stream/delete.js b/frontend/js/app/nginx/stream/delete.js deleted file mode 100644 index 71eff18c..00000000 --- a/frontend/js/app/nginx/stream/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.Streams.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxStream(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/stream/form.ejs b/frontend/js/app/nginx/stream/form.ejs deleted file mode 100644 index eb80c373..00000000 --- a/frontend/js/app/nginx/stream/form.ejs +++ /dev/null @@ -1,55 +0,0 @@ - diff --git a/frontend/js/app/nginx/stream/form.js b/frontend/js/app/nginx/stream/form.js deleted file mode 100644 index be8fc8bc..00000000 --- a/frontend/js/app/nginx/stream/form.js +++ /dev/null @@ -1,84 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const StreamModel = require('../../../models/stream'); -const template = require('./form.ejs'); - -require('jquery-serializejson'); -require('jquery-mask-plugin'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - forwarding_host: 'input[name="forwarding_host"]', - type_error: '.forward-type-error', - buttons: '.modal-footer button', - switches: '.custom-switch-input', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - 'change @ui.switches': function () { - this.ui.type_error.hide(); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - if (!data.tcp_forwarding && !data.udp_forwarding) { - this.ui.type_error.show(); - return; - } - - // Manipulate - data.incoming_port = parseInt(data.incoming_port, 10); - data.forwarding_port = parseInt(data.forwarding_port, 10); - data.tcp_forwarding = !!data.tcp_forwarding; - data.udp_forwarding = !!data.udp_forwarding; - - let method = App.Api.Nginx.Streams.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.Streams.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxStream(); - } - }); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new StreamModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/stream/list/item.ejs b/frontend/js/app/nginx/stream/list/item.ejs deleted file mode 100644 index a8ff83d4..00000000 --- a/frontend/js/app/nginx/stream/list/item.ejs +++ /dev/null @@ -1,53 +0,0 @@ - -
- -
- - -
- <%- incoming_port %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forwarding_host %>:<%- forwarding_port %>
- - -
- <% if (tcp_forwarding) { %> - <%- i18n('streams', 'tcp') %> - <% } - if (udp_forwarding) { %> - <%- i18n('streams', 'udp') %> - <% } %> -
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/stream/list/item.js b/frontend/js/app/nginx/stream/list/item.js deleted file mode 100644 index a6892ee2..00000000 --- a/frontend/js/app/nginx/stream/list/item.js +++ /dev/null @@ -1,54 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.Streams[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.Streams.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamDeleteConfirm(this.model); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('streams'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/stream/list/main.ejs b/frontend/js/app/nginx/stream/list/main.ejs deleted file mode 100644 index 5304f614..00000000 --- a/frontend/js/app/nginx/stream/list/main.ejs +++ /dev/null @@ -1,13 +0,0 @@ - -   - <%- i18n('streams', 'incoming-port') %> - <%- i18n('str', 'destination') %> - <%- i18n('streams', 'protocol') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/stream/list/main.js b/frontend/js/app/nginx/stream/list/main.js deleted file mode 100644 index 36be621d..00000000 --- a/frontend/js/app/nginx/stream/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('streams') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/stream/main.ejs b/frontend/js/app/nginx/stream/main.ejs deleted file mode 100644 index 7dc0dbe8..00000000 --- a/frontend/js/app/nginx/stream/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('streams', 'title') %>

-
- - - <% if (showAddButton) { %> - <%- i18n('streams', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/stream/main.js b/frontend/js/app/nginx/stream/main.js deleted file mode 100644 index 8a86e583..00000000 --- a/frontend/js/app/nginx/stream/main.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const StreamModel = require('../../../models/stream'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-stream', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.Streams.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new StreamModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxStream(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('streams'); - - this.showChildView('list_region', new EmptyView({ - title: App.i18n('streams', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('streams', 'add') : null, - btn_color: 'blue', - permission: 'streams', - action: function () { - App.Controller.showNginxStreamForm(); - } - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('streams', 'help-title'), App.i18n('streams', 'help-content')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('streams') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/router.js b/frontend/js/app/router.js deleted file mode 100644 index a036bfc5..00000000 --- a/frontend/js/app/router.js +++ /dev/null @@ -1,19 +0,0 @@ -const AppRouter = require('marionette.approuter'); -const Controller = require('./controller'); - -module.exports = AppRouter.default.extend({ - controller: Controller, - appRoutes: { - users: 'showUsers', - logout: 'logout', - 'nginx/proxy': 'showNginxProxy', - 'nginx/redirection': 'showNginxRedirection', - 'nginx/404': 'showNginxDead', - 'nginx/stream': 'showNginxStream', - 'nginx/access': 'showNginxAccess', - 'nginx/certificates': 'showNginxCertificates', - 'audit-log': 'showAuditLog', - 'settings': 'showSettings', - '*default': 'showDashboard' - } -}); diff --git a/frontend/js/app/settings/default-site/main.ejs b/frontend/js/app/settings/default-site/main.ejs deleted file mode 100644 index 126c9d0a..00000000 --- a/frontend/js/app/settings/default-site/main.ejs +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/frontend/js/app/settings/default-site/main.js b/frontend/js/app/settings/default-site/main.js deleted file mode 100644 index 06a45b8b..00000000 --- a/frontend/js/app/settings/default-site/main.js +++ /dev/null @@ -1,69 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./main.ejs'); - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - options: '.option-item', - value: 'input[name="value"]', - redirect: '.redirect-input', - html: '.html-content' - }, - - events: { - 'change @ui.value': function (e) { - let val = this.ui.value.filter(':checked').val(); - this.ui.options.hide(); - this.ui.options.filter('.option-' + val).show(); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - - let val = this.ui.value.filter(':checked').val(); - - // Clear redirect field before validation - if (val !== 'redirect') { - this.ui.redirect.val('').attr('required', false); - } else { - this.ui.redirect.attr('required', true); - } - - this.ui.html.attr('required', val === 'html'); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - data.id = this.model.get('id'); - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - App.Api.Settings.update(data) - .then(result => { - view.model.set(result); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - onRender: function () { - this.ui.value.trigger('change'); - } -}); diff --git a/frontend/js/app/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs deleted file mode 100644 index 4f81b450..00000000 --- a/frontend/js/app/settings/list/item.ejs +++ /dev/null @@ -1,21 +0,0 @@ - -
<%- name %>
-
- <%- description %> -
- - -
- <% if (id === 'default-site') { %> - <%- i18n('settings', 'default-site-' + value) %> - <% } %> -
- - - - \ No newline at end of file diff --git a/frontend/js/app/settings/list/item.js b/frontend/js/app/settings/list/item.js deleted file mode 100644 index 03f9ac05..00000000 --- a/frontend/js/app/settings/list/item.js +++ /dev/null @@ -1,23 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showSettingForm(this.model); - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/settings/list/main.ejs b/frontend/js/app/settings/list/main.ejs deleted file mode 100644 index c96e923a..00000000 --- a/frontend/js/app/settings/list/main.ejs +++ /dev/null @@ -1,8 +0,0 @@ - - <%- i18n('str', 'name') %> - <%- i18n('str', 'value') %> -   - - - - diff --git a/frontend/js/app/settings/list/main.js b/frontend/js/app/settings/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/settings/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/settings/main.ejs b/frontend/js/app/settings/main.ejs deleted file mode 100644 index 2b02769f..00000000 --- a/frontend/js/app/settings/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
-

<%- i18n('settings', 'title') %>

-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/settings/main.js b/frontend/js/app/settings/main.js deleted file mode 100644 index 96b2941f..00000000 --- a/frontend/js/app/settings/main.js +++ /dev/null @@ -1,48 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const SettingModel = require('../../models/setting'); -const ListView = require('./list/main'); -const ErrorView = require('../error/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'settings', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - onRender: function () { - let view = this; - - App.Api.Settings.getAll() - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new SettingModel.Collection(response) - })); - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showSettings(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/tokens.js b/frontend/js/app/tokens.js deleted file mode 100644 index 4a56bcab..00000000 --- a/frontend/js/app/tokens.js +++ /dev/null @@ -1,126 +0,0 @@ -const STORAGE_NAME = 'nginx-proxy-manager-tokens'; - -/** - * @returns {Array} - */ -const getStorageTokens = function () { - let json = window.localStorage.getItem(STORAGE_NAME); - if (json) { - try { - return JSON.parse(json); - } catch (err) { - return []; - } - } - - return []; -}; - -/** - * @param {Array} tokens - */ -const setStorageTokens = function (tokens) { - window.localStorage.setItem(STORAGE_NAME, JSON.stringify(tokens)); -}; - -const Tokens = { - - /** - * @returns {Number} - */ - getTokenCount: () => { - return getStorageTokens().length; - }, - - /** - * @returns {Object} t,n - */ - getTopToken: () => { - let tokens = getStorageTokens(); - if (tokens && tokens.length) { - return tokens[0]; - } - - return null; - }, - - /** - * @returns {String} - */ - getNextTokenName: () => { - let tokens = getStorageTokens(); - if (tokens && tokens.length > 1 && typeof tokens[1] !== 'undefined' && typeof tokens[1].n !== 'undefined') { - return tokens[1].n; - } - - return null; - }, - - /** - * - * @param {String} token - * @param {String} [name] - * @returns {Number} - */ - addToken: (token, name) => { - // Get top token and if it's the same, ignore this call - let top = Tokens.getTopToken(); - if (!top || top.t !== token) { - let tokens = getStorageTokens(); - tokens.unshift({t: token, n: name || null}); - setStorageTokens(tokens); - } - - return Tokens.getTokenCount(); - }, - - /** - * @param {String} token - * @returns {Boolean} - */ - setCurrentToken: token => { - let tokens = getStorageTokens(); - if (tokens.length) { - tokens[0].t = token; - setStorageTokens(tokens); - return true; - } - - return false; - }, - - /** - * @param {String} name - * @returns {Boolean} - */ - setCurrentName: name => { - let tokens = getStorageTokens(); - if (tokens.length) { - tokens[0].n = name; - setStorageTokens(tokens); - return true; - } - - return false; - }, - - /** - * @returns {Number} - */ - dropTopToken: () => { - let tokens = getStorageTokens(); - tokens.shift(); - setStorageTokens(tokens); - return tokens.length; - }, - - /** - * - */ - clearTokens: () => { - window.localStorage.removeItem(STORAGE_NAME); - } - -}; - -module.exports = Tokens; diff --git a/frontend/js/app/ui/footer/main.ejs b/frontend/js/app/ui/footer/main.ejs deleted file mode 100644 index 562e71c2..00000000 --- a/frontend/js/app/ui/footer/main.ejs +++ /dev/null @@ -1,16 +0,0 @@ -
- -
- <%- i18n('main', 'version', {version: getVersion()}) %> - <%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %> - <%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %> -
-
diff --git a/frontend/js/app/ui/footer/main.js b/frontend/js/app/ui/footer/main.js deleted file mode 100644 index 73f515e6..00000000 --- a/frontend/js/app/ui/footer/main.js +++ /dev/null @@ -1,14 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); -const Cache = require('../../cache'); - -module.exports = Mn.View.extend({ - className: 'container', - template: template, - - templateContext: { - getVersion: function () { - return Cache.version || '0.0.0'; - } - } -}); diff --git a/frontend/js/app/ui/header/main.ejs b/frontend/js/app/ui/header/main.ejs deleted file mode 100644 index 18ed2b6a..00000000 --- a/frontend/js/app/ui/header/main.ejs +++ /dev/null @@ -1,34 +0,0 @@ - diff --git a/frontend/js/app/ui/header/main.js b/frontend/js/app/ui/header/main.js deleted file mode 100644 index 9779b45c..00000000 --- a/frontend/js/app/ui/header/main.js +++ /dev/null @@ -1,67 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const i18n = require('../../i18n'); -const Cache = require('../../cache'); -const Controller = require('../../controller'); -const Tokens = require('../../tokens'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'header', - className: 'header', - template: template, - - ui: { - link: 'a', - details: 'a.edit-details', - password: 'a.change-password' - }, - - events: { - 'click @ui.details': function (e) { - e.preventDefault(); - Controller.showUserForm(Cache.User); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - Controller.showUserPasswordForm(Cache.User); - }, - - 'click @ui.link': function (e) { - e.preventDefault(); - let href = $(e.currentTarget).attr('href'); - - switch (href) { - case '/': - Controller.showDashboard(); - break; - case '/logout': - Controller.logout(); - break; - } - } - }, - - templateContext: { - getUserField: function (field, default_val) { - return Cache.User.get(field) || default_val; - }, - - getRole: function () { - return i18n('roles', Cache.User.isAdmin() ? 'admin' : 'user'); - }, - - getLogoutText: function () { - if (Tokens.getTokenCount() > 1) { - return i18n('main', 'sign-in-as', {name: Tokens.getNextTokenName()}); - } - - return i18n('str', 'sign-out'); - } - }, - - initialize: function () { - this.listenTo(Cache.User, 'change', this.render); - } -}); diff --git a/frontend/js/app/ui/main.ejs b/frontend/js/app/ui/main.ejs deleted file mode 100644 index b62c3acd..00000000 --- a/frontend/js/app/ui/main.ejs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- -
-
-
- -
- -
- - diff --git a/frontend/js/app/ui/main.js b/frontend/js/app/ui/main.js deleted file mode 100644 index c90c61d5..00000000 --- a/frontend/js/app/ui/main.js +++ /dev/null @@ -1,98 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); -const HeaderView = require('./header/main'); -const MenuView = require('./menu/main'); -const FooterView = require('./footer/main'); -const Cache = require('../cache'); - -module.exports = Mn.View.extend({ - id: 'app', - className: 'page', - template: template, - modal_setup: false, - - modal: null, - - ui: { - modal: '#modal-dialog' - }, - - regions: { - header_region: { - el: '#header', - replaceElement: true - }, - menu_region: { - el: '#menu', - replaceElement: true - }, - footer_region: '.footer', - app_content_region: '#app-content', - modal_region: '#modal-dialog' - }, - - /** - * @param {Object} view - */ - showAppContent: function (view) { - this.showChildView('app_content_region', view); - }, - - /** - * @param {Object} view - * @param {Function} [show_callback] - * @param {Function} [shown_callback] - */ - showModalDialog: function (view, show_callback, shown_callback) { - this.showChildView('modal_region', view); - let modal = this.getRegion('modal_region').$el.modal('show'); - - modal.on('hidden.bs.modal', function (/*e*/) { - if (show_callback) { - modal.off('show.bs.modal', show_callback); - } - - if (shown_callback) { - modal.off('shown.bs.modal', shown_callback); - } - - modal.off('hidden.bs.modal'); - view.destroy(); - }); - - if (show_callback) { - modal.on('show.bs.modal', show_callback); - } - - if (shown_callback) { - modal.on('shown.bs.modal', shown_callback); - } - }, - - /** - * - * @param {Function} [hidden_callback] - */ - closeModal: function (hidden_callback) { - let modal = this.getRegion('modal_region').$el.modal('hide'); - - if (hidden_callback) { - modal.on('hidden.bs.modal', hidden_callback); - } - }, - - onRender: function () { - this.showChildView('header_region', new HeaderView({ - model: Cache.User - })); - - this.showChildView('menu_region', new MenuView()); - this.showChildView('footer_region', new FooterView()); - }, - - reset: function () { - this.getRegion('header_region').reset(); - this.getRegion('footer_region').reset(); - this.getRegion('modal_region').reset(); - } -}); diff --git a/frontend/js/app/ui/menu/main.ejs b/frontend/js/app/ui/menu/main.ejs deleted file mode 100644 index 671b4e3b..00000000 --- a/frontend/js/app/ui/menu/main.ejs +++ /dev/null @@ -1,52 +0,0 @@ -
-
-
- -
-
-
diff --git a/frontend/js/app/ui/menu/main.js b/frontend/js/app/ui/menu/main.js deleted file mode 100644 index dabe26d3..00000000 --- a/frontend/js/app/ui/menu/main.js +++ /dev/null @@ -1,39 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const Controller = require('../../controller'); -const Cache = require('../../cache'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'menu', - className: 'header collapse d-lg-flex p-0', - template: template, - - ui: { - links: 'a' - }, - - events: { - 'click @ui.links': function (e) { - let href = $(e.currentTarget).attr('href'); - if (href !== '#') { - e.preventDefault(); - Controller.navigate(href, true); - } - } - }, - - templateContext: { - isAdmin: function () { - return Cache.User.isAdmin(); - }, - - canShow: function (perm) { - return Cache.User.isAdmin() || Cache.User.canView(perm); - } - }, - - initialize: function () { - this.listenTo(Cache.User, 'change', this.render); - } -}); diff --git a/frontend/js/app/user/delete.ejs b/frontend/js/app/user/delete.ejs deleted file mode 100644 index c10532ef..00000000 --- a/frontend/js/app/user/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/user/delete.js b/frontend/js/app/user/delete.js deleted file mode 100644 index e8ed5c32..00000000 --- a/frontend/js/app/user/delete.js +++ /dev/null @@ -1,34 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./delete.ejs'); -const App = require('../main'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Users.delete(this.model.get('id')) - .then(() => { - App.Controller.showUsers(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/user/form.ejs b/frontend/js/app/user/form.ejs deleted file mode 100644 index aeb268f7..00000000 --- a/frontend/js/app/user/form.ejs +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/frontend/js/app/user/form.js b/frontend/js/app/user/form.js deleted file mode 100644 index ef92ec3e..00000000 --- a/frontend/js/app/user/form.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const template = require('./form.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - error: '.secret-error' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.error.hide(); - let view = this; - let data = this.ui.form.serializeJSON(); - - let show_password = this.model.get('email') === 'admin@example.com'; - - // admin@example.com is not allowed - if (data.email === 'admin@example.com') { - this.ui.error.text(App.i18n('users', 'default_error')).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - return; - } - - // Manipulate - data.roles = []; - if ((this.model.get('id') === App.Cache.User.get('id') && this.model.isAdmin()) || (typeof data.is_admin !== 'undefined' && data.is_admin)) { - data.roles.push('admin'); - delete data.is_admin; - } - - data.is_disabled = typeof data.is_disabled !== 'undefined' ? !!data.is_disabled : false; - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - let method = App.Api.Users.create; - - if (this.model.get('id')) { - // edit - method = App.Api.Users.update; - data.id = this.model.get('id'); - } - - method(data) - .then(result => { - if (result.id === App.Cache.User.get('id')) { - App.Cache.User.set(result); - } - - if (view.model.get('id') !== App.Cache.User.get('id')) { - App.Controller.showUsers(); - } - - view.model.set(result); - App.UI.closeModal(function () { - if (method === App.Api.Users.create) { - // Show permissions dialog immediately - App.Controller.showUserPermissions(view.model); - } else if (show_password) { - App.Controller.showUserPasswordForm(view.model); - } - }); - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - templateContext: function () { - let view = this; - - return { - isSelf: function () { - return view.model.get('id') === App.Cache.User.get('id'); - }, - - isAdmin: function () { - return App.Cache.User.isAdmin(); - }, - - isAdminUser: function () { - return view.model.isAdmin(); - }, - - isDisabled: function () { - return view.model.isDisabled(); - } - }; - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new UserModel.Model(); - } - } -}); diff --git a/frontend/js/app/user/password.ejs b/frontend/js/app/user/password.ejs deleted file mode 100644 index a45cc7ed..00000000 --- a/frontend/js/app/user/password.ejs +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/frontend/js/app/user/password.js b/frontend/js/app/user/password.js deleted file mode 100644 index 84030750..00000000 --- a/frontend/js/app/user/password.js +++ /dev/null @@ -1,69 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const template = require('./password.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - newSecretError: '.new-secret-error', - generalError: '#error-info', - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.newSecretError.hide(); - this.ui.generalError.hide(); - let form = this.ui.form.serializeJSON(); - - if (form.new_password1 !== form.new_password2) { - this.ui.newSecretError.text('Passwords do not match!').show(); - return; - } - - let data = { - type: 'password', - current: form.current_password, - secret: form.new_password1 - }; - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - App.Api.Users.setPassword(this.model.get('id'), data) - .then(() => { - App.UI.closeModal(); - App.Controller.showUsers(); - }) - .catch(err => { - // Change error message to make it a little clearer - if (err.message === 'Invalid password') { - err.message = 'Current password is invalid'; - } - this.ui.generalError.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - isSelf: function () { - return App.Cache.User.get('id') === this.model.get('id'); - }, - - templateContext: function () { - return { - isSelf: this.isSelf.bind(this) - }; - }, - - onRender: function () { - this.ui.newSecretError.hide(); - this.ui.generalError.hide(); - }, -}); diff --git a/frontend/js/app/user/permissions.ejs b/frontend/js/app/user/permissions.ejs deleted file mode 100644 index b6161796..00000000 --- a/frontend/js/app/user/permissions.ejs +++ /dev/null @@ -1,68 +0,0 @@ - diff --git a/frontend/js/app/user/permissions.js b/frontend/js/app/user/permissions.js deleted file mode 100644 index af8049ce..00000000 --- a/frontend/js/app/user/permissions.js +++ /dev/null @@ -1,95 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const template = require('./permissions.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - error: '.secret-error' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - if (view.model.isAdmin()) { - // Force some attributes for admin - data = _.assign({}, data, { - access_lists: 'manage', - dead_hosts: 'manage', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - streams: 'manage', - certificates: 'manage' - }); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Users.setPermissions(view.model.get('id'), data) - .then(() => { - if (view.model.get('id') === App.Cache.User.get('id')) { - App.Cache.User.set({permissions: data}); - } - - view.model.set({permissions: data}); - App.UI.closeModal(); - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - templateContext: function () { - let perms = this.model.get('permissions'); - let is_admin = this.model.isAdmin(); - - return { - getPerm: function (key) { - if (perms !== null && typeof perms[key] !== 'undefined') { - return perms[key]; - } - - return null; - }, - - getPermProps: function (key, item, forced_admin) { - if (forced_admin && is_admin) { - return 'checked disabled'; - } else if (is_admin) { - return 'disabled'; - } else if (perms !== null && typeof perms[key] !== 'undefined' && perms[key] === item) { - return 'checked'; - } - - return ''; - }, - - isAdmin: function () { - return is_admin; - } - }; - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new UserModel.Model(); - } - } -}); diff --git a/frontend/js/app/users/list/item.ejs b/frontend/js/app/users/list/item.ejs deleted file mode 100644 index fab5585b..00000000 --- a/frontend/js/app/users/list/item.ejs +++ /dev/null @@ -1,45 +0,0 @@ - -
- -
- - -
<%- name %>
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- email %>
- - -
- <% - var r = []; - roles.map(function(role) { - if (role) { - r.push(i18n('roles', role)); - } - }); - %> - <%- r.join(', ') %> -
- - - - diff --git a/frontend/js/app/users/list/item.js b/frontend/js/app/users/list/item.js deleted file mode 100644 index 4645a5c4..00000000 --- a/frontend/js/app/users/list/item.js +++ /dev/null @@ -1,68 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const Tokens = require('../../tokens'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit-user', - permissions: 'a.edit-permissions', - password: 'a.set-password', - login: 'a.login', - delete: 'a.delete-user' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showUserForm(this.model); - }, - - 'click @ui.permissions': function (e) { - e.preventDefault(); - App.Controller.showUserPermissions(this.model); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - App.Controller.showUserPasswordForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showUserDeleteConfirm(this.model); - }, - - 'click @ui.login': function (e) { - e.preventDefault(); - - if (App.Cache.User.get('id') !== this.model.get('id')) { - this.ui.login.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Users.loginAs(this.model.get('id')) - .then(res => { - Tokens.addToken(res.token, res.user.nickname || res.user.name); - window.location = '/'; - window.location.reload(); - }) - .catch(err => { - alert(err.message); - this.ui.login.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } - }, - - templateContext: { - isSelf: function () { - return App.Cache.User.get('id') === this.id; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/users/list/main.ejs b/frontend/js/app/users/list/main.ejs deleted file mode 100644 index c85c9cb1..00000000 --- a/frontend/js/app/users/list/main.ejs +++ /dev/null @@ -1,10 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('str', 'email') %> - <%- i18n('str', 'roles') %> -   - - - - diff --git a/frontend/js/app/users/list/main.js b/frontend/js/app/users/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/users/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/users/main.ejs b/frontend/js/app/users/main.ejs deleted file mode 100644 index 892cb83f..00000000 --- a/frontend/js/app/users/main.ejs +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
-

<%- i18n('users', 'title') %>

-
- - <%- i18n('users', 'add') %> -
-
-
-
-
-
- -
-
- -
-
diff --git a/frontend/js/app/users/main.js b/frontend/js/app/users/main.js deleted file mode 100644 index 42cb41ef..00000000 --- a/frontend/js/app/users/main.js +++ /dev/null @@ -1,78 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const ListView = require('./list/main'); -const ErrorView = require('../error/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'users', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - dimmer: '.dimmer', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Users.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new UserModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showUsers(); - } - })); - - console.error(err); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showUserForm(new UserModel.Model()); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['permissions'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - onRender: function () { - let view = this; - - view.fetch(['permissions']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showData(response); - } - }) - .catch(err => { - view.showError(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json deleted file mode 100644 index 896a9633..00000000 --- a/frontend/js/i18n/messages.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "en": { - "str": { - "email-address": "Email address", - "username": "Username", - "password": "Password", - "sign-in": "Sign in", - "sign-out": "Sign out", - "try-again": "Try again", - "name": "Name", - "email": "Email", - "roles": "Roles", - "created-on": "Created: {date}", - "save": "Save", - "cancel": "Cancel", - "close": "Close", - "enable": "Enable", - "disable": "Disable", - "sure": "Yes I'm Sure", - "disabled": "Disabled", - "choose-file": "Choose file", - "source": "Source", - "destination": "Destination", - "ssl": "SSL", - "access": "Access", - "public": "Public", - "edit": "Edit", - "delete": "Delete", - "logs": "Logs", - "status": "Status", - "online": "Online", - "offline": "Offline", - "unknown": "Unknown", - "expires": "Expires", - "value": "Value", - "please-wait": "Please wait...", - "all": "All", - "any": "Any" - }, - "login": { - "title": "Login to your account" - }, - "main": { - "app": "Nginx Proxy Manager", - "version": "v{version}", - "welcome": "Welcome to Nginx Proxy Manager", - "logged-in": "You are logged in as {name}", - "unknown-error": "Error loading stuff. Please reload the app.", - "unknown-user": "Unknown User", - "sign-in-as": "Sign back in as {name}" - }, - "roles": { - "title": "Roles", - "admin": "Administrator", - "user": "Apache Helicopter" - }, - "menu": { - "dashboard": "Dashboard", - "hosts": "Hosts" - }, - "footer": { - "fork-me": "Fork me on Github", - "copy": "© 2022 jc21.com.", - "theme": "Theme by Tabler" - }, - "dashboard": { - "title": "Hi {name}" - }, - "all-hosts": { - "empty-subtitle": "{manage, select, true{Why don't you create one?} other{And you don't have permission to create one.}}", - "details": "Details", - "enable-ssl": "Enable SSL", - "force-ssl": "Force SSL", - "http2-support": "HTTP/2 Support", - "domain-names": "Domain Names", - "cert-provider": "Certificate Provider", - "block-exploits": "Block Common Exploits", - "caching-enabled": "Cache Assets", - "ssl-certificate": "SSL Certificate", - "none": "None", - "new-cert": "Request a new SSL Certificate", - "with-le": "with Let's Encrypt", - "no-ssl": "This host will not use HTTPS", - "advanced": "Advanced", - "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", - "advanced-config": "Custom Nginx Configuration", - "advanced-config-var-headline": "These proxy details are available as nginx variables:", - "advanced-config-header-info": "Please note, that any add_header or set_header directives added here will not be used by nginx. You will have to add a custom location '/' and add the header in the custom config there.", - "hsts-enabled": "HSTS Enabled", - "hsts-subdomains": "HSTS Subdomains", - "locations": "Custom locations" - }, - "locations": { - "new_location": "Add location", - "path": "/path", - "location_label": "Define location", - "delete": "Delete" - }, - "ssl": { - "letsencrypt": "Let's Encrypt", - "other": "Custom", - "none": "HTTP only", - "letsencrypt-email": "Email Address for Let's Encrypt", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", - "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", - "hosts-warning": "These domains must be already configured to point to this installation", - "no-wildcard-without-dns": "Cannot request Let's Encrypt Certificate for wildcard domains when not using DNS challenge", - "dns-challenge": "Use a DNS Challenge", - "certbot-warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.", - "dns-provider": "DNS Provider", - "please-choose": "Please Choose...", - "credentials-file-content": "Credentials File Content", - "credentials-file-content-info": "This plugin requires a configuration file containing an API token or other credentials to your provider", - "stored-as-plaintext-info": "This data will be stored as plaintext in the database and in a file!", - "propagation-seconds": "Propagation Seconds", - "propagation-seconds-info": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.", - "processing-info": "Processing... This might take a few minutes.", - "passphrase-protection-support-info": "Key files protected with a passphrase are not supported." - }, - "proxy-hosts": { - "title": "Proxy Hosts", - "empty": "There are no Proxy Hosts", - "add": "Add Proxy Host", - "form-title": "{id, select, undefined{New} other{Edit}} Proxy Host", - "forward-scheme": "Scheme", - "forward-host": "Forward Hostname / IP", - "forward-port": "Forward Port", - "delete": "Delete Proxy Host", - "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", - "help-title": "What is a Proxy Host?", - "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", - "access-list": "Access List", - "allow-websocket-upgrade": "Websockets Support", - "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", - "custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path", - "search": "Search Host…" - }, - "redirection-hosts": { - "title": "Redirection Hosts", - "empty": "There are no Redirection Hosts", - "add": "Add Redirection Host", - "form-title": "{id, select, undefined{New} other{Edit}} Redirection Host", - "forward-scheme": "Scheme", - "forward-http-status-code": "HTTP Code", - "forward-domain": "Forward Domain", - "preserve-path": "Preserve Path", - "delete": "Delete Redirection Host", - "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?", - "help-title": "What is a Redirection Host?", - "help-content": "A Redirection Host will redirect requests from the incoming domain and push the viewer to another domain.\nThe most common reason to use this type of host is when your website changes domains but you still have search engine or referrer links pointing to the old domain.", - "search": "Search Host…" - }, - "dead-hosts": { - "title": "404 Hosts", - "empty": "There are no 404 Hosts", - "add": "Add 404 Host", - "form-title": "{id, select, undefined{New} other{Edit}} 404 Host", - "delete": "Delete 404 Host", - "delete-confirm": "Are you sure you want to delete this 404 Host?", - "help-title": "What is a 404 Host?", - "help-content": "A 404 Host is simply a host setup that shows a 404 page.\nThis can be useful when your domain is listed in search engines and you want to provide a nicer error page or specifically to tell the search indexers that the domain pages no longer exist.\nAnother benefit of having this host is to track the logs for hits to it and view the referrers.", - "search": "Search Host…" - }, - "streams": { - "title": "Streams", - "empty": "There are no Streams", - "add": "Add Stream", - "form-title": "{id, select, undefined{New} other{Edit}} Stream", - "incoming-port": "Incoming Port", - "forwarding-host": "Forward Host", - "forwarding-port": "Forward Port", - "tcp-forwarding": "TCP Forwarding", - "udp-forwarding": "UDP Forwarding", - "forward-type-error": "At least one type of protocol must be enabled", - "protocol": "Protocol", - "tcp": "TCP", - "udp": "UDP", - "delete": "Delete Stream", - "delete-confirm": "Are you sure you want to delete this Stream?", - "help-title": "What is a Stream?", - "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", - "search": "Search Incoming Port…" - }, - "certificates": { - "title": "SSL Certificates", - "empty": "There are no SSL Certificates", - "add": "Add SSL Certificate", - "form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate", - "delete": "Delete SSL Certificate", - "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.", - "help-title": "SSL Certificates", - "help-content": "SSL certificates (correctly known as TLS Certificates) are a form of encryption key which allows your site to be encrypted for the end user.\nNPM uses a service called Let's Encrypt to issue SSL certificates for free.\nIf you have any sort of personal information, passwords, or sensitive data behind NPM, it's probably a good idea to use a certificate.\nNPM also supports DNS authentication for if you're not running your site facing the internet, or if you just want a wildcard certificate.", - "other-certificate": "Certificate", - "other-certificate-key": "Certificate Key", - "other-intermediate-certificate": "Intermediate Certificate", - "force-renew": "Renew Now", - "test-reachability": "Test Server Reachability", - "reachability-title": "Test Server Reachability", - "reachability-info": "Test whether the domains are reachable from the public internet using Site24x7. This is not necessary when using the DNS Challenge.", - "reachability-failed-to-reach-api": "Communication with the API failed, is NPM running correctly?", - "reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.", - "reachability-ok": "Your server is reachable and creating certificates should be possible.", - "reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.", - "reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "download": "Download", - "renew-title": "Renew Let'sEncrypt Certificate", - "search": "Search Certificate…" - }, - "access-lists": { - "title": "Access Lists", - "empty": "There are no Access Lists", - "add": "Add Access List", - "form-title": "{id, select, undefined{New} other{Edit}} Access List", - "delete": "Delete Access List", - "delete-confirm": "Are you sure you want to delete this access list?", - "public": "Publicly Accessible", - "public-sub": "No Access Restrictions", - "help-title": "What is an Access List?", - "help-content": "Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or that you want to protect from access by unknown clients.", - "item-count": "{count} {count, select, 1{User} other{Users}}", - "client-count": "{count} {count, select, 1{Rule} other{Rules}}", - "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}", - "delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion.", - "details": "Details", - "authorization": "Authorization", - "access": "Access", - "satisfy": "Satisfy", - "satisfy-any": "Satisfy Any", - "pass-auth": "Pass Auth to Host", - "access-add": "Add", - "auth-add": "Add", - "search": "Search Access…" - }, - "users": { - "title": "Users", - "default_error": "Default email address must be changed", - "add": "Add User", - "nickname": "Nickname", - "full-name": "Full Name", - "edit-details": "Edit Details", - "change-password": "Change Password", - "edit-permissions": "Edit Permissions", - "sign-in-as": "Sign in as User", - "form-title": "{id, select, undefined{New} other{Edit}} User", - "delete": "Delete {name, select, undefined{User} other{{name}}}", - "delete-confirm": "Are you sure you want to delete {name}?", - "password-title": "Change Password{self, select, false{ for {name}} other{}}", - "current-password": "Current Password", - "new-password": "New Password", - "confirm-password": "Confirm Password", - "permissions-title": "Permissions for {name}", - "admin-perms": "This user is an Administrator and some items cannot be altered", - "perms-visibility": "Item Visibility", - "perms-visibility-user": "Created Items Only", - "perms-visibility-all": "All Items", - "perm-manage": "Manage", - "perm-view": "View Only", - "perm-hidden": "Hidden", - "search": "Search User…" - }, - "audit-log": { - "title": "Audit Log", - "empty": "There are no logs.", - "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here.", - "proxy-host": "Proxy Host", - "redirection-host": "Redirection Host", - "dead-host": "404 Host", - "stream": "Stream", - "user": "User", - "certificate": "Certificate", - "access-list": "Access List", - "created": "Created {name}", - "updated": "Updated {name}", - "deleted": "Deleted {name}", - "enabled": "Enabled {name}", - "disabled": "Disabled {name}", - "renewed": "Renewed {name}", - "meta-title": "Details for Event", - "view-meta": "View Details", - "date": "Date", - "search": "Search Log…" - }, - "settings": { - "title": "Settings", - "default-site": "Default Site", - "default-site-congratulations": "Congratulations Page", - "default-site-404": "404 Page", - "default-site-html": "Custom Page", - "default-site-redirect": "Redirect" - } - } -} diff --git a/frontend/js/index.js b/frontend/js/index.js deleted file mode 100644 index 3d817d71..00000000 --- a/frontend/js/index.js +++ /dev/null @@ -1,119 +0,0 @@ -// This has to exist here so that Webpack picks it up -import '../scss/styles.scss'; - -window.tabler = { - colors: { - 'blue': '#467fcf', - 'blue-darkest': '#0e1929', - 'blue-darker': '#1c3353', - 'blue-dark': '#3866a6', - 'blue-light': '#7ea5dd', - 'blue-lighter': '#c8d9f1', - 'blue-lightest': '#edf2fa', - 'azure': '#45aaf2', - 'azure-darkest': '#0e2230', - 'azure-darker': '#1c4461', - 'azure-dark': '#3788c2', - 'azure-light': '#7dc4f6', - 'azure-lighter': '#c7e6fb', - 'azure-lightest': '#ecf7fe', - 'indigo': '#6574cd', - 'indigo-darkest': '#141729', - 'indigo-darker': '#282e52', - 'indigo-dark': '#515da4', - 'indigo-light': '#939edc', - 'indigo-lighter': '#d1d5f0', - 'indigo-lightest': '#f0f1fa', - 'purple': '#a55eea', - 'purple-darkest': '#21132f', - 'purple-darker': '#42265e', - 'purple-dark': '#844bbb', - 'purple-light': '#c08ef0', - 'purple-lighter': '#e4cff9', - 'purple-lightest': '#f6effd', - 'pink': '#f66d9b', - 'pink-darkest': '#31161f', - 'pink-darker': '#622c3e', - 'pink-dark': '#c5577c', - 'pink-light': '#f999b9', - 'pink-lighter': '#fcd3e1', - 'pink-lightest': '#fef0f5', - 'red': '#e74c3c', - 'red-darkest': '#2e0f0c', - 'red-darker': '#5c1e18', - 'red-dark': '#b93d30', - 'red-light': '#ee8277', - 'red-lighter': '#f8c9c5', - 'red-lightest': '#fdedec', - 'orange': '#fd9644', - 'orange-darkest': '#331e0e', - 'orange-darker': '#653c1b', - 'orange-dark': '#ca7836', - 'orange-light': '#feb67c', - 'orange-lighter': '#fee0c7', - 'orange-lightest': '#fff5ec', - 'yellow': '#f1c40f', - 'yellow-darkest': '#302703', - 'yellow-darker': '#604e06', - 'yellow-dark': '#c19d0c', - 'yellow-light': '#f5d657', - 'yellow-lighter': '#fbedb7', - 'yellow-lightest': '#fef9e7', - 'lime': '#7bd235', - 'lime-darkest': '#192a0b', - 'lime-darker': '#315415', - 'lime-dark': '#62a82a', - 'lime-light': '#a3e072', - 'lime-lighter': '#d7f2c2', - 'lime-lightest': '#f2fbeb', - 'green': '#5eba00', - 'green-darkest': '#132500', - 'green-darker': '#264a00', - 'green-dark': '#4b9500', - 'green-light': '#8ecf4d', - 'green-lighter': '#cfeab3', - 'green-lightest': '#eff8e6', - 'teal': '#2bcbba', - 'teal-darkest': '#092925', - 'teal-darker': '#11514a', - 'teal-dark': '#22a295', - 'teal-light': '#6bdbcf', - 'teal-lighter': '#bfefea', - 'teal-lightest': '#eafaf8', - 'cyan': '#17a2b8', - 'cyan-darkest': '#052025', - 'cyan-darker': '#09414a', - 'cyan-dark': '#128293', - 'cyan-light': '#5dbecd', - 'cyan-lighter': '#b9e3ea', - 'cyan-lightest': '#e8f6f8', - 'gray': '#868e96', - 'gray-darkest': '#1b1c1e', - 'gray-darker': '#36393c', - 'gray-light': '#aab0b6', - 'gray-lighter': '#dbdde0', - 'gray-lightest': '#f3f4f5', - 'gray-dark': '#343a40', - 'gray-dark-darkest': '#0a0c0d', - 'gray-dark-darker': '#15171a', - 'gray-dark-dark': '#2a2e33', - 'gray-dark-light': '#717579', - 'gray-dark-lighter': '#c2c4c6', - 'gray-dark-lightest': '#ebebec' - } -}; - -String.prototype.toHtmlEntities = function() { - return this.replace(/./gm, function(s) { - // return "&#" + s.charCodeAt(0) + ";"; - return (s.match(/[a-z0-9\s]+/i)) ? s : "&#" + s.charCodeAt(0) + ";"; - }); -}; - -require('tabler-core'); - -const App = require('./app/main'); - -$(document).ready(() => { - App.start(); -}); diff --git a/frontend/js/lib/helpers.js b/frontend/js/lib/helpers.js deleted file mode 100644 index 21ce7424..00000000 --- a/frontend/js/lib/helpers.js +++ /dev/null @@ -1,26 +0,0 @@ -const numeral = require('numeral'); -const moment = require('moment'); - -module.exports = { - - /** - * @param {Integer} number - * @returns {String} - */ - niceNumber: function (number) { - return numeral(number).format('0,0'); - }, - - /** - * @param {String|Number} date - * @param {String} format - * @returns {String} - */ - formatDbDate: function (date, format) { - if (typeof date === 'number') { - return moment.unix(date).format(format); - } - - return moment(date).format(format); - } -}; diff --git a/frontend/js/lib/marionette.js b/frontend/js/lib/marionette.js deleted file mode 100644 index c88368f8..00000000 --- a/frontend/js/lib/marionette.js +++ /dev/null @@ -1,15 +0,0 @@ -const _ = require('underscore'); -const Mn = require('backbone.marionette'); -const i18n = require('../app/i18n'); -const Helpers = require('./helpers'); -const TemplateCache = require('marionette.templatecache'); - -Mn.setRenderer(function (template, data, view) { - data = _.clone(data); - data.i18n = i18n; - data.formatDbDate = Helpers.formatDbDate; - - return TemplateCache.default.render.call(this, template, data, view); -}); - -module.exports = Mn; diff --git a/frontend/js/login.js b/frontend/js/login.js deleted file mode 100644 index 0094e2a2..00000000 --- a/frontend/js/login.js +++ /dev/null @@ -1,5 +0,0 @@ -const App = require('./login/main'); - -$(document).ready(() => { - App.start(); -}); diff --git a/frontend/js/login/main.js b/frontend/js/login/main.js deleted file mode 100644 index 03fdc7e5..00000000 --- a/frontend/js/login/main.js +++ /dev/null @@ -1,14 +0,0 @@ -const Mn = require('backbone.marionette'); -const LoginView = require('./ui/login'); - -const App = Mn.Application.extend({ - region: '#login', - UI: null, - - onStart: function (/*app, options*/) { - this.getRegion().show(new LoginView()); - } -}); - -const app = new App(); -module.exports = app; diff --git a/frontend/js/login/ui/login.ejs b/frontend/js/login/ui/login.ejs deleted file mode 100644 index b6f52b7a..00000000 --- a/frontend/js/login/ui/login.ejs +++ /dev/null @@ -1,37 +0,0 @@ -
-
- -
-
diff --git a/frontend/js/login/ui/login.js b/frontend/js/login/ui/login.js deleted file mode 100644 index 757eb4e3..00000000 --- a/frontend/js/login/ui/login.js +++ /dev/null @@ -1,42 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const template = require('./login.ejs'); -const Api = require('../../app/api'); -const i18n = require('../../app/i18n'); - -module.exports = Mn.View.extend({ - template: template, - className: 'page-single', - - ui: { - form: 'form', - identity: 'input[name="identity"]', - secret: 'input[name="secret"]', - error: '.secret-error', - button: 'button' - }, - - events: { - 'submit @ui.form': function (e) { - e.preventDefault(); - this.ui.button.addClass('btn-loading').prop('disabled', true); - this.ui.error.hide(); - - Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true) - .then(() => { - window.location = '/'; - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.button.removeClass('btn-loading').prop('disabled', false); - }); - } - }, - - templateContext: { - i18n: i18n, - getVersion: function () { - return $('#login').data('version'); - } - } -}); diff --git a/frontend/js/models/access-list.js b/frontend/js/models/access-list.js deleted file mode 100644 index 0c2c4abe..00000000 --- a/frontend/js/models/access-list.js +++ /dev/null @@ -1,25 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - name: '', - items: [], - clients: [], - // The following are expansions: - owner: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/audit-log.js b/frontend/js/models/audit-log.js deleted file mode 100644 index c929a0bd..00000000 --- a/frontend/js/models/audit-log.js +++ /dev/null @@ -1,18 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - name: '' - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/certificate.js b/frontend/js/models/certificate.js deleted file mode 100644 index c7d0b2d9..00000000 --- a/frontend/js/models/certificate.js +++ /dev/null @@ -1,38 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - provider: '', - nice_name: '', - domain_names: [], - expires_on: null, - meta: {}, - // The following are expansions: - owner: null, - proxy_hosts: [], - redirection_hosts: [], - dead_hosts: [] - }; - }, - - /** - * @returns {Boolean} - */ - hasSslFiles: function () { - let meta = this.get('meta'); - return typeof meta['certificate'] !== 'undefined' && meta['certificate'] && typeof meta['certificate_key'] !== 'undefined' && meta['certificate_key']; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/dead-host.js b/frontend/js/models/dead-host.js deleted file mode 100644 index 98ceef29..00000000 --- a/frontend/js/models/dead-host.js +++ /dev/null @@ -1,32 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - certificate_id: 0, - ssl_forced: false, - http2_support: false, - hsts_enabled: false, - hsts_subdomains: false, - enabled: true, - meta: {}, - advanced_config: '', - // The following are expansions: - owner: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/proxy-host-location.js b/frontend/js/models/proxy-host-location.js deleted file mode 100644 index 2a35059f..00000000 --- a/frontend/js/models/proxy-host-location.js +++ /dev/null @@ -1,35 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function() { - return { - opened: false, - path: '', - advanced_config: '', - forward_scheme: 'http', - forward_host: '', - forward_port: '80' - } - }, - - toJSON() { - const r = Object.assign({}, this.attributes); - delete r.opened; - return r; - }, - - toggleVisibility: function () { - this.save({ - opened: !this.get('opened') - }); - } -}) - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model - }) -} \ No newline at end of file diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js deleted file mode 100644 index b82d09fe..00000000 --- a/frontend/js/models/proxy-host.js +++ /dev/null @@ -1,40 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - forward_scheme: 'http', - forward_host: '', - forward_port: null, - access_list_id: 0, - certificate_id: 0, - ssl_forced: false, - hsts_enabled: false, - hsts_subdomains: false, - caching_enabled: false, - allow_websocket_upgrade: false, - block_exploits: false, - http2_support: false, - advanced_config: '', - enabled: true, - meta: {}, - // The following are expansions: - owner: null, - access_list: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/redirection-host.js b/frontend/js/models/redirection-host.js deleted file mode 100644 index 1d0b0de2..00000000 --- a/frontend/js/models/redirection-host.js +++ /dev/null @@ -1,37 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - forward_http_code: 0, - forward_scheme: null, - forward_domain_name: '', - preserve_path: true, - certificate_id: 0, - ssl_forced: false, - hsts_enabled: false, - hsts_subdomains: false, - block_exploits: false, - http2_support: false, - advanced_config: '', - enabled: true, - meta: {}, - // The following are expansions: - owner: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/setting.js b/frontend/js/models/setting.js deleted file mode 100644 index c70a4e9c..00000000 --- a/frontend/js/models/setting.js +++ /dev/null @@ -1,22 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - name: '', - description: '', - value: null, - meta: [] - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/stream.js b/frontend/js/models/stream.js deleted file mode 100644 index ba035429..00000000 --- a/frontend/js/models/stream.js +++ /dev/null @@ -1,29 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - incoming_port: null, - forwarding_host: null, - forwarding_port: null, - tcp_forwarding: true, - udp_forwarding: false, - enabled: true, - meta: {}, - // The following are expansions: - owner: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/user.js b/frontend/js/models/user.js deleted file mode 100644 index a8e4ed9e..00000000 --- a/frontend/js/models/user.js +++ /dev/null @@ -1,54 +0,0 @@ -const _ = require('underscore'); -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - name: '', - nickname: '', - email: '', - is_disabled: false, - roles: [], - permissions: null - }; - }, - - /** - * @returns {Boolean} - */ - isAdmin: function () { - return _.indexOf(this.get('roles'), 'admin') !== -1; - }, - - /** - * Checks if the perm has either `view` or `manage` value - * - * @param {String} item - * @returns {Boolean} - */ - canView: function (item) { - let permissions = this.get('permissions'); - return permissions !== null && typeof permissions[item] !== 'undefined' && ['view', 'manage'].indexOf(permissions[item]) !== -1; - }, - - /** - * Checks if the perm has `manage` value - * - * @param {String} item - * @returns {Boolean} - */ - canManage: function (item) { - let permissions = this.get('permissions'); - return permissions !== null && typeof permissions[item] !== 'undefined' && permissions[item] === 'manage'; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/package.json b/frontend/package.json index 198e6c93..770eb7d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,47 +1,111 @@ { - "name": "nginx-proxy-manager", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "js/index.js", - "devDependencies": { - "@babel/core": "^7.9.0", - "babel-core": "^6.26.3", - "babel-loader": "^8.1.0", - "babel-preset-env": "^1.7.0", - "backbone": "^1.4.0", - "backbone.marionette": "^4.1.2", - "copy-webpack-plugin": "^5.1.1", - "css-loader": "^3.5.0", - "ejs-lint": "^1.0.1", - "ejs-loader": "^0.3.6", - "ejs-webpack-loader": "^2.2.2", - "file-loader": "^6.0.0", - "html-webpack-plugin": "^4.0.4", - "imports-loader": "^0.8.0", - "jquery": "^3.5.0", - "jquery-mask-plugin": "^1.14.16", - "jquery-serializejson": "^2.9.0", - "marionette.approuter": "^1.0.2", - "marionette.templatecache": "^1.0.0", - "messageformat": "^2.3.0", - "messageformat-loader": "^0.8.1", - "mini-css-extract-plugin": "^0.9.0", - "moment": "^2.24.0", - "node-sass": "^6.0.1", - "nodemon": "^2.0.2", - "numeral": "^2.0.6", - "sass-loader": "10.2.0", - "style-loader": "^1.1.3", - "tabler-ui": "git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813", - "underscore": "^1.12.1", - "webpack": "^4.42.1", - "webpack-cli": "^3.3.11", - "webpack-visualizer-plugin": "^0.1.11" - }, - "scripts": { - "build": "webpack --mode production", - "watch": "webpack --watch --mode development" - }, - "author": "Jamie Curnow ", - "license": "MIT" + "name": "nginxproxymanager", + "version": "3.0.0", + "private": true, + "dependencies": { + "@chakra-ui/react": "^1.8.3", + "@emotion/react": "^11", + "@emotion/styled": "^11.6.0", + "@testing-library/jest-dom": "5.16.2", + "@testing-library/react": "12.1.2", + "@types/humps": "^2.0.1", + "@types/jest": "27.4.0", + "@types/lodash": "4.14.178", + "@types/node": "17.0.15", + "@types/react": "17.0.39", + "@types/react-dom": "17.0.11", + "@types/react-router-dom": "5.3.3", + "@types/react-table": "^7.7.9", + "@types/styled-components": "5.1.22", + "@typescript-eslint/eslint-plugin": "^5.10.2", + "@typescript-eslint/parser": "^5.10.2", + "babel-eslint": "^10.1.0", + "classnames": "^2.3.1", + "country-flag-icons": "^1.4.20", + "date-fns": "2.28.0", + "eslint": "^8.8.0", + "eslint-config-prettier": "^8.3.0", + "eslint-config-react-app": "^7.0.0", + "eslint-loader": "^4.0.2", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", + "formik": "^2.2.9", + "framer-motion": "^6", + "humps": "^2.0.1", + "jest-date-mock": "1.0.8", + "jest-fetch-mock": "3.0.3", + "jest-junit": "^13.0.0", + "jest-localstorage-mock": "2.4.18", + "jest-runner-eslint": "1.0.0", + "lodash": "4.17.21", + "moment": "2.29.1", + "node-sass": "^7.0.1", + "prettier": "2.5.1", + "query-string": "7.1.1", + "react": "^17.0.2", + "react-async": "10.0.1", + "react-dom": "17.0.2", + "react-focus-lock": "^2.8.1", + "react-icons": "^4.3.1", + "react-intl": "^5.24.6", + "react-markdown": "^8.0.1", + "react-query": "^3.34.19", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.0", + "react-table": "^7.7.0", + "rooks": "5.10.0", + "tmp": "^0.2.1", + "typescript": "^4.5.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "lint": "yarn jest --config jest.eslint.js --watch", + "lint:ci": "yarn lint --watchAll=false", + "test": "react-scripts test", + "test:coverage": "yarn test --coverage --watchAll=false", + "test:ci": "yarn test:coverage", + "eject": "react-scripts eject", + "prettier": "prettier \"**/*.+(js|json|yml|css|ts|tsx)\"", + "format": "yarn prettier -- --write", + "lint:fix": "eslint --fix --ext .ts --ext .tsx .", + "locale-extract": "formatjs extract 'src/**/*.tsx'", + "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang", + "check-locales": "./check-locales.js" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "jest": { + "globalSetup": "/globalSetup.js", + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/testUtils/*" + ], + "coverageThreshold": { + "global": { + "branches": 0, + "functions": 0, + "lines": 0, + "statements": 0 + } + } + }, + "devDependencies": { + "@formatjs/cli": "^4.8.2", + "@types/country-flag-icons": "^1.2.0" + } } diff --git a/frontend/app-images/default-avatar.jpg b/frontend/public/images/default-avatar.jpg similarity index 100% rename from frontend/app-images/default-avatar.jpg rename to frontend/public/images/default-avatar.jpg diff --git a/frontend/app-images/favicons/android-chrome-192x192.png b/frontend/public/images/favicon/android-chrome-192x192.png similarity index 100% rename from frontend/app-images/favicons/android-chrome-192x192.png rename to frontend/public/images/favicon/android-chrome-192x192.png diff --git a/frontend/app-images/favicons/android-chrome-512x512.png b/frontend/public/images/favicon/android-chrome-512x512.png similarity index 100% rename from frontend/app-images/favicons/android-chrome-512x512.png rename to frontend/public/images/favicon/android-chrome-512x512.png diff --git a/frontend/app-images/favicons/apple-touch-icon.png b/frontend/public/images/favicon/apple-touch-icon.png similarity index 100% rename from frontend/app-images/favicons/apple-touch-icon.png rename to frontend/public/images/favicon/apple-touch-icon.png diff --git a/frontend/app-images/favicons/browserconfig.xml b/frontend/public/images/favicon/browserconfig.xml similarity index 100% rename from frontend/app-images/favicons/browserconfig.xml rename to frontend/public/images/favicon/browserconfig.xml diff --git a/frontend/app-images/favicons/favicon-16x16.png b/frontend/public/images/favicon/favicon-16x16.png similarity index 100% rename from frontend/app-images/favicons/favicon-16x16.png rename to frontend/public/images/favicon/favicon-16x16.png diff --git a/frontend/app-images/favicons/favicon-32x32.png b/frontend/public/images/favicon/favicon-32x32.png similarity index 100% rename from frontend/app-images/favicons/favicon-32x32.png rename to frontend/public/images/favicon/favicon-32x32.png diff --git a/frontend/app-images/favicons/favicon.ico b/frontend/public/images/favicon/favicon.ico similarity index 100% rename from frontend/app-images/favicons/favicon.ico rename to frontend/public/images/favicon/favicon.ico diff --git a/frontend/app-images/favicons/mstile-150x150.png b/frontend/public/images/favicon/mstile-150x150.png similarity index 100% rename from frontend/app-images/favicons/mstile-150x150.png rename to frontend/public/images/favicon/mstile-150x150.png diff --git a/frontend/app-images/favicons/safari-pinned-tab.svg b/frontend/public/images/favicon/safari-pinned-tab.svg similarity index 100% rename from frontend/app-images/favicons/safari-pinned-tab.svg rename to frontend/public/images/favicon/safari-pinned-tab.svg diff --git a/frontend/app-images/favicons/site.webmanifest b/frontend/public/images/favicon/site.webmanifest similarity index 100% rename from frontend/app-images/favicons/site.webmanifest rename to frontend/public/images/favicon/site.webmanifest diff --git a/frontend/app-images/logo-256.png b/frontend/public/images/logo-256.png similarity index 100% rename from frontend/app-images/logo-256.png rename to frontend/public/images/logo-256.png diff --git a/frontend/public/images/logo-bold-horizontal-grey.svg b/frontend/public/images/logo-bold-horizontal-grey.svg new file mode 100644 index 00000000..c87396f6 --- /dev/null +++ b/frontend/public/images/logo-bold-horizontal-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/logo-no-text.svg b/frontend/public/images/logo-no-text.svg new file mode 100644 index 00000000..dc3c1163 --- /dev/null +++ b/frontend/public/images/logo-no-text.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/logo-text-horizontal-grey.png b/frontend/public/images/logo-text-horizontal-grey.png new file mode 100644 index 0000000000000000000000000000000000000000..057a83d1b7a433e79f9d7b022488f29aee1c1e0c GIT binary patch literal 22203 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%0Ai+xv4%rSdX>J#T}H=Vzde z7MCY4i9%)64EC%u>+_pL+ViE3n#mTjk`UKwz#|dxN99%`;NZb9%pJxG3?NfbvdoYv z-~M?l8n0~8mB!E0gZZz7*&J?EJW**#p(Kg4-xjW-o6q2hV4?)HFO)tLP2za-3Pz%` z%*#LwsI2v;N`gmn4-ll3$f_)29m$Im7f@-Iv1!d__(2vEWPaEL9-kgKW;7OI7>4O! zR#**W1_W#^B!o6!4KI&<=xV{dHrWb9;E_;s`~<#GszqA=u6`~$7nRkZ%80a3Nx--n zR$@d+57{7V(^~QA8^bRaOuRFe-sAqN`QjLcVU~{7GY{GYiS^d-yD`k$tY?{E`&&=?q~QhUt7dArtc>C(ihbMY^9296tuI3BU`BbWet1n2w`U zQu(&y{2Ks<0Q~8F+~>EpGu`P|O)ld27=~dwj!vruUvT!Z56^RR)HMJPIXm9jTi+Rm zVVI8HO!Tr-8QD9_N}}jqV--@ + + + + + Nginx Proxy Manager + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/frontend/scss/custom.scss b/frontend/scss/custom.scss deleted file mode 100644 index 4037dcf6..00000000 --- a/frontend/scss/custom.scss +++ /dev/null @@ -1,42 +0,0 @@ -$primary-color: #2bcbba; - -.loader { - color: $primary-color; -} - -a { - color: $primary-color; -} - -a:hover { - color: darken($primary-color, 10%); -} - -.dropdown-header { - padding-left: 1rem; -} - -.dropdown-item.active, .dropdown-item:active { - background-color: $primary-color; -} - -.custom-switch-input:checked ~ .custom-switch-indicator { - background: $primary-color; -} - -.min-100 { - min-height: 100px; -} - -.card-options .dropdown-menu a:not(.btn) { - margin-left: 0; -} - -.wrap { - display: flex; - flex-wrap: wrap; -} - -.col-login { - max-width: 48rem; -} \ No newline at end of file diff --git a/frontend/scss/fonts.scss b/frontend/scss/fonts.scss deleted file mode 100644 index f0ec1b73..00000000 --- a/frontend/scss/fonts.scss +++ /dev/null @@ -1,39 +0,0 @@ -/* source-sans-pro-regular - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 400; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-italic - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: italic; - font-weight: 400; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-700italic - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: italic; - font-weight: 700; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-700 - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 700; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} diff --git a/frontend/scss/selectize.scss b/frontend/scss/selectize.scss deleted file mode 100644 index e12d5b69..00000000 --- a/frontend/scss/selectize.scss +++ /dev/null @@ -1,196 +0,0 @@ -.selectize-dropdown-header { - position: relative; - padding: 5px 8px; - background: #f8f8f8; - border-bottom: 1px solid #d0d0d0; - -webkit-border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} - -.selectize-dropdown-header-close { - position: absolute; - top: 50%; - right: 8px; - margin-top: -12px; - font-size: 20px !important; - line-height: 20px; - color: #303030; - opacity: 0.4; -} - -.selectize-dropdown-header-close:hover { - color: #000000; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup { - float: left; - border-top: 0 none; - border-right: 1px solid #f2f2f2; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { - border-right: 0 none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:before { - display: none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup-header { - border-top: 0 none; -} - -.selectize-control.plugin-remove_button [data-value] { - position: relative; - padding-right: 24px !important; -} - -.selectize-control.plugin-remove_button [data-value] .remove { - position: absolute; - top: 0; - right: 0; - bottom: 0; - display: inline-block; - width: 17px; - padding: 2px 0 0 0; - font-size: 12px; - font-weight: bold; - color: inherit; - text-align: center; - text-decoration: none; - vertical-align: middle; - border-left: 1px solid #0073bb; - -webkit-border-radius: 0 2px 2px 0; - -moz-border-radius: 0 2px 2px 0; - border-radius: 0 2px 2px 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-control.plugin-remove_button [data-value] .remove:hover { - background: rgba(0, 0, 0, 0.05); -} - -.selectize-control.plugin-remove_button [data-value].active .remove { - border-left-color: #00578d; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { - background: none; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove { - border-left-color: #aaaaaa; -} - -.selectize-control { - position: relative; -} - -.selectize-dropdown { - font-family: inherit; - font-size: 13px; - -webkit-font-smoothing: inherit; - line-height: 18px; - color: #303030; -} - -.selectize-control.single { - display: inline-block; - cursor: text; - background: #ffffff; -} - -.selectize-dropdown { - position: absolute; - z-index: 10; - margin: -1px 0 0 0; - background: #ffffff; - border: 1px solid #d0d0d0; - border-top: 0 none; - -webkit-border-radius: 0 0 3px 3px; - -moz-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-dropdown [data-selectable] { - overflow: hidden; - cursor: pointer; -} - -.selectize-dropdown [data-selectable] .highlight { - background: rgba(125, 168, 208, 0.2); - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; -} - -.selectize-dropdown [data-selectable], -.selectize-dropdown .optgroup-header { - padding: 5px 8px; -} - -.selectize-dropdown .optgroup:first-child .optgroup-header { - border-top: 0 none; -} - -.selectize-dropdown .optgroup-header { - color: #303030; - cursor: default; - background: #ffffff; -} - -.selectize-dropdown .active { - color: #495c68; - background-color: #f5fafd; -} - -.selectize-dropdown .active.create { - color: #495c68; -} - -.selectize-dropdown .create { - color: rgba(48, 48, 48, 0.5); -} - -.selectize-dropdown-content { - max-height: 200px; - overflow-x: hidden; - overflow-y: auto; - - .title { - font-weight: bold; - } - - .description { - padding-left: 16px; - } -} - -.selectize-dropdown .optgroup-header { - padding-top: 7px; - font-size: 0.85em; - font-weight: bold; -} - -.selectize-dropdown .optgroup { - border-top: 1px solid #f0f0f0; -} - -.selectize-dropdown .optgroup:first-child { - border-top: 0 none; -} - -.custom-select { - height: auto; -} diff --git a/frontend/scss/styles.scss b/frontend/scss/styles.scss deleted file mode 100644 index 52733097..00000000 --- a/frontend/scss/styles.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import "~tabler-ui/dist/assets/css/dashboard"; -@import "tabler-extra"; -@import "fonts"; -@import "selectize"; -@import "custom"; - -/* Before any JS content is loaded */ -#app > .loader, #login > .loader, .container > .loader { - position: absolute; - left: 49%; - top: 40%; - display: block; -} - -.no-js-warning { - margin-top: 100px; -} diff --git a/frontend/scss/tabler-extra.scss b/frontend/scss/tabler-extra.scss deleted file mode 100644 index 3ddd0ed4..00000000 --- a/frontend/scss/tabler-extra.scss +++ /dev/null @@ -1,170 +0,0 @@ -$teal: #2bcbba; -$yellow: #f1c40f; -$blue: #467fcf; -$pink: #f66d9b; - -.tag { - margin-bottom: .5em; - margin-right: .5em; -} - -.tag.hover-green:hover, .tag.hover-green:active, .tag.hover-green:focus { - background-color: #5eba00; - cursor: pointer; - color: #fff; -} - -.tag.hover-red:hover, .tag.hover-red:active, .tag.hover-red:focus { - background-color: #cd201f; - cursor: pointer; - color: #fff; -} - -/* For Card bodies where I don't want padding */ -.card-body.no-padding { - padding: 0; -} - -/* For some reason this class doesn't have 'display: flex' when it should. https://preview.tabler.io/docs/buttons.html#list-of-buttons */ -.btn-list { - display: flex; -} - -/* Teal Outline Buttons */ -.btn-outline-teal { - color: $teal; - background-color: transparent; - background-image: none; - border-color: $teal; -} - -.btn-outline-teal:hover { - color: #fff; - background-color: $teal; - border-color: $teal; -} - -.btn-outline-teal:not(:disabled):not(.disabled):active, .btn-outline-teal:not(:disabled):not(.disabled).active, .show > .btn-outline-teal.dropdown-toggle { - color: #fff; - background-color: $teal; - border-color: $teal; -} - -.tag.hover-teal:hover, .tag.hover-teal:active, .tag.hover-teal:focus { - background-color: $teal; - color: #fff; - cursor: pointer; -} - -/* Yellow Outline Buttons */ -.btn-outline-yellow { - color: $yellow; - background-color: transparent; - background-image: none; - border-color: $yellow; -} - -.btn-outline-yellow:hover { - color: #fff; - background-color: $yellow; - border-color: $yellow; -} - -.btn-outline-yellow:not(:disabled):not(.disabled):active, .btn-outline-yellow:not(:disabled):not(.disabled).active, .show > .btn-outline-yellow.dropdown-toggle { - color: #fff; - background-color: $yellow; - border-color: $yellow; -} - -.tag.hover-yellow:hover, .tag.hover-yellow:active, .tag.hover-yellow:focus { - background-color: $yellow; - cursor: pointer; - color: #fff; -} - -/* Blue Outline Buttons */ -.btn-outline-blue { - color: $blue; - background-color: transparent; - background-image: none; - border-color: $blue; -} - -.btn-outline-blue:hover { - color: #fff; - background-color: $blue; - border-color: $blue; -} - -.btn-outline-blue:not(:disabled):not(.disabled):active, .btn-outline-blue:not(:disabled):not(.disabled).active, .show > .btn-outline-blue.dropdown-toggle { - color: #fff; - background-color: $blue; - border-color: $blue; -} - -.tag.hover-blue:hover, .tag.hover-blue:active, .tag.hover-blue:focus { - background-color: $blue; - cursor: pointer; - color: #fff; -} - -/* Pink Outline Buttons */ -.btn-outline-pink { - color: $pink; - background-color: transparent; - background-image: none; - border-color: $pink; -} - -.btn-outline-pink:hover { - color: #fff; - background-color: $pink; - border-color: $pink; -} - -.btn-outline-pink:not(:disabled):not(.disabled):active, .btn-outline-pink:not(:disabled):not(.disabled).active, .show > .btn-outline-pink.dropdown-toggle { - color: #fff; - background-color: $pink; - border-color: $pink; -} - -.tag.hover-pink:hover, .tag.hover-pink:active, .tag.hover-pink:focus { - background-color: $pink; - cursor: pointer; -} - -/* dimmer */ - -.dimmer .loader { - margin-top: 50px; -} - -/* modal tabs */ - -.modal-body.has-tabs { - padding: 0; - - .nav-tabs { - margin: 0; - } - - .tab-content { - padding: 1rem; - } -} - -/* modal wide */ - -@media (min-width: 576px) { - .modal-dialog.wide { - max-width: 700px; - margin: 1.75rem auto; - } -} - - -/* Form mod */ - -textarea.form-control.text-monospace { - font-size: 12px; -} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 00000000..1aad9cc7 --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; + +import * as ReactDOM from "react-dom"; + +import App from "./App"; + +it("renders without crashing", () => { + const div = document.createElement("div"); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..558611c7 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,31 @@ +import { ChakraProvider } from "@chakra-ui/react"; +import { AuthProvider, LocaleProvider } from "context"; +import { intl } from "locale"; +import { RawIntlProvider } from "react-intl"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { ReactQueryDevtools } from "react-query/devtools"; + +import Router from "./Router"; +import lightTheme from "./theme/customTheme"; + +// Create a client +const queryClient = new QueryClient(); + +function App() { + return ( + + + + + + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 00000000..755579a4 --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,81 @@ +import { lazy, Suspense } from "react"; + +import { SiteWrapper, SpinnerPage, Unhealthy } from "components"; +import { useAuthState, useLocaleState } from "context"; +import { useHealth } from "hooks"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; + +const AccessLists = lazy(() => import("pages/AccessLists")); +const AuditLog = lazy(() => import("pages/AuditLog")); +const Certificates = lazy(() => import("pages/Certificates")); +const CertificateAuthorities = lazy( + () => import("pages/CertificateAuthorities"), +); +const Dashboard = lazy(() => import("pages/Dashboard")); +const DNSProviders = lazy(() => import("pages/DNSProviders")); +const Hosts = lazy(() => import("pages/Hosts")); +const HostTemplates = lazy(() => import("pages/HostTemplates")); +const Login = lazy(() => import("pages/Login")); +const GeneralSettings = lazy(() => import("pages/Settings")); +const Setup = lazy(() => import("pages/Setup")); +const Users = lazy(() => import("pages/Users")); + +function Router() { + const health = useHealth(); + const { authenticated } = useAuthState(); + const { locale } = useLocaleState(); + const Spinner = ; + + if (health.isLoading) { + return Spinner; + } + + if (health.isError || !health.data?.healthy) { + return ; + } + + if (health.data?.healthy && !health.data?.setup) { + return ( + + + + ); + } + + if (!authenticated) { + return ( + + + + ); + } + + return ( + + + + + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + + + + + ); +} + +export default Router; diff --git a/frontend/src/api/npm/base.ts b/frontend/src/api/npm/base.ts new file mode 100644 index 00000000..8d6e4719 --- /dev/null +++ b/frontend/src/api/npm/base.ts @@ -0,0 +1,95 @@ +import { camelizeKeys, decamelizeKeys } from "humps"; +import AuthStore from "modules/AuthStore"; +import * as queryString from "query-string"; + +const contentTypeHeader = "Content-Type"; + +interface BuildUrlArgs { + url: string; + params?: queryString.StringifiableRecord; +} +function buildUrl({ url, params }: BuildUrlArgs) { + const endpoint = url.replace(/^\/|\/$/g, ""); + const apiParams = params ? `?${queryString.stringify(params)}` : ""; + const apiUrl = `/api/${endpoint}${apiParams}`; + return apiUrl; +} + +function buildAuthHeader(): Record | undefined { + if (AuthStore.token) { + return { Authorization: `Bearer ${AuthStore.token.token}` }; + } + return {}; +} + +function buildBody(data?: Record) { + if (data) { + return JSON.stringify(decamelizeKeys(data)); + } +} + +async function processResponse(response: Response) { + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error.message); + } + return camelizeKeys(payload) as any; +} + +interface GetArgs { + url: string; + params?: queryString.StringifiableRecord; +} + +export async function get( + { url, params }: GetArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url, params }); + const method = "GET"; + const signal = abortController?.signal; + const headers = buildAuthHeader(); + const response = await fetch(apiUrl, { method, headers, signal }); + return processResponse(response); +} + +interface PostArgs { + url: string; + data?: any; +} + +export async function post( + { url, data }: PostArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "POST"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const body = buildBody(data); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} + +interface PutArgs { + url: string; + data?: any; +} +export async function put( + { url, data }: PutArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "PUT"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const body = buildBody(data); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} diff --git a/frontend/src/api/npm/createCertificateAuthority.ts b/frontend/src/api/npm/createCertificateAuthority.ts new file mode 100644 index 00000000..1822c744 --- /dev/null +++ b/frontend/src/api/npm/createCertificateAuthority.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function createCertificateAuthority( + data: CertificateAuthority, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/certificate-authorities", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/createDNSProvider.ts b/frontend/src/api/npm/createDNSProvider.ts new file mode 100644 index 00000000..6a8821d4 --- /dev/null +++ b/frontend/src/api/npm/createDNSProvider.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function createDNSProvider( + data: DNSProvider, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/dns-providers", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/createUser.ts b/frontend/src/api/npm/createUser.ts new file mode 100644 index 00000000..8c893240 --- /dev/null +++ b/frontend/src/api/npm/createUser.ts @@ -0,0 +1,30 @@ +import * as api from "./base"; +import { User } from "./models"; + +export interface AuthOptions { + type: string; + secret: string; +} + +export interface NewUser { + name: string; + nickname: string; + email: string; + isDisabled: boolean; + auth: AuthOptions; + capabilities: string[]; +} + +export async function createUser( + data: NewUser, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/users", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getCertificateAuthorities.ts b/frontend/src/api/npm/getCertificateAuthorities.ts new file mode 100644 index 00000000..e956bf52 --- /dev/null +++ b/frontend/src/api/npm/getCertificateAuthorities.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { CertificateAuthoritiesResponse } from "./responseTypes"; + +export async function getCertificateAuthorities( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "certificate-authorities", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getCertificateAuthority.ts b/frontend/src/api/npm/getCertificateAuthority.ts new file mode 100644 index 00000000..10674f14 --- /dev/null +++ b/frontend/src/api/npm/getCertificateAuthority.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function getCertificateAuthority( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/certificate-authorities/${id}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getCertificates.ts b/frontend/src/api/npm/getCertificates.ts new file mode 100644 index 00000000..ace316d8 --- /dev/null +++ b/frontend/src/api/npm/getCertificates.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { CertificatesResponse } from "./responseTypes"; + +export async function getCertificates( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "certificates", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getDNSProvider.ts b/frontend/src/api/npm/getDNSProvider.ts new file mode 100644 index 00000000..380e5d78 --- /dev/null +++ b/frontend/src/api/npm/getDNSProvider.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function getDNSProvider( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/dns-providers/${id}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getDNSProviders.ts b/frontend/src/api/npm/getDNSProviders.ts new file mode 100644 index 00000000..51028a56 --- /dev/null +++ b/frontend/src/api/npm/getDNSProviders.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { DNSProvidersResponse } from "./responseTypes"; + +export async function getDNSProviders( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "dns-providers", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getDNSProvidersAcmesh.ts b/frontend/src/api/npm/getDNSProvidersAcmesh.ts new file mode 100644 index 00000000..03654c61 --- /dev/null +++ b/frontend/src/api/npm/getDNSProvidersAcmesh.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { DNSProvidersAcmesh } from "./models"; + +export async function getDNSProvidersAcmesh( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "dns-providers/acmesh", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getHealth.ts b/frontend/src/api/npm/getHealth.ts new file mode 100644 index 00000000..9c6d77b9 --- /dev/null +++ b/frontend/src/api/npm/getHealth.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { HealthResponse } from "./responseTypes"; + +export async function getHealth( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getHostTemplates.ts b/frontend/src/api/npm/getHostTemplates.ts new file mode 100644 index 00000000..dc91d666 --- /dev/null +++ b/frontend/src/api/npm/getHostTemplates.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { HostTemplatesResponse } from "./responseTypes"; + +export async function getHostTemplates( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "host-templates", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getHosts.ts b/frontend/src/api/npm/getHosts.ts new file mode 100644 index 00000000..4529fb8e --- /dev/null +++ b/frontend/src/api/npm/getHosts.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { HostsResponse } from "./responseTypes"; + +export async function getHosts( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "hosts", + params: { limit, offset, sort, expand: "user,certificate", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getSettings.ts b/frontend/src/api/npm/getSettings.ts new file mode 100644 index 00000000..6e07806d --- /dev/null +++ b/frontend/src/api/npm/getSettings.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { SettingsResponse } from "./responseTypes"; + +export async function getSettings( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "settings", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getToken.ts b/frontend/src/api/npm/getToken.ts new file mode 100644 index 00000000..ff7c5557 --- /dev/null +++ b/frontend/src/api/npm/getToken.ts @@ -0,0 +1,24 @@ +import * as api from "./base"; +import { TokenResponse } from "./responseTypes"; + +interface Options { + payload: { + type: string; + identity: string; + secret: string; + }; +} + +export async function getToken( + { payload }: Options, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/tokens", + data: payload, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getUser.ts b/frontend/src/api/npm/getUser.ts new file mode 100644 index 00000000..a6485f91 --- /dev/null +++ b/frontend/src/api/npm/getUser.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { User } from "./models"; + +export async function getUser( + id: number | string = "me", + params = {}, +): Promise { + const userId = id ? id : "me"; + const { result } = await api.get({ + url: `/users/${userId}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getUsers.ts b/frontend/src/api/npm/getUsers.ts new file mode 100644 index 00000000..89b163d5 --- /dev/null +++ b/frontend/src/api/npm/getUsers.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { UsersResponse } from "./responseTypes"; + +export async function getUsers( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "users", + params: { limit, offset, sort, expand: "capabilities", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/helpers.ts b/frontend/src/api/npm/helpers.ts new file mode 100644 index 00000000..da6b8ca8 --- /dev/null +++ b/frontend/src/api/npm/helpers.ts @@ -0,0 +1,34 @@ +import { decamelize } from "humps"; + +/** + * This will convert a react-table sort object into + * a string that the backend api likes: + * name.asc,id.desc + */ +export function tableSortToAPI(sortBy: any): string | undefined { + if (sortBy?.length > 0) { + const strs: string[] = []; + sortBy.map((item: any) => { + strs.push(decamelize(item.id) + "." + (item.desc ? "desc" : "asc")); + return undefined; + }); + return strs.join(","); + } + return; +} + +/** + * This will convert a react-table filters object into + * a string that the backend api likes: + * name:contains=jam + */ +export function tableFiltersToAPI(filters: any): { [key: string]: string } { + const items: { [key: string]: string } = {}; + if (filters?.length > 0) { + filters.map((item: any) => { + items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value; + return undefined; + }); + } + return items; +} diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts new file mode 100644 index 00000000..faf6fe88 --- /dev/null +++ b/frontend/src/api/npm/index.ts @@ -0,0 +1,24 @@ +export * from "./createCertificateAuthority"; +export * from "./createDNSProvider"; +export * from "./createUser"; +export * from "./getCertificateAuthorities"; +export * from "./getCertificateAuthority"; +export * from "./getCertificates"; +export * from "./getDNSProvider"; +export * from "./getDNSProviders"; +export * from "./getDNSProvidersAcmesh"; +export * from "./getHealth"; +export * from "./getHosts"; +export * from "./getHostTemplates"; +export * from "./getSettings"; +export * from "./getToken"; +export * from "./getUser"; +export * from "./getUsers"; +export * from "./helpers"; +export * from "./models"; +export * from "./refreshToken"; +export * from "./responseTypes"; +export * from "./setAuth"; +export * from "./setCertificateAuthority"; +export * from "./setDNSProvider"; +export * from "./setUser"; diff --git a/frontend/src/api/npm/models.ts b/frontend/src/api/npm/models.ts new file mode 100644 index 00000000..b739ecdf --- /dev/null +++ b/frontend/src/api/npm/models.ts @@ -0,0 +1,123 @@ +export interface Sort { + field: string; + direction: "ASC" | "DESC"; +} + +export interface UserAuth { + id: number; + userId: number; + type: string; + createdOn: number; + updatedOn: number; +} + +export interface User { + id: number; + name: string; + nickname: string; + email: string; + createdOn: number; + updatedOn: number; + gravatarUrl: string; + isDisabled: boolean; + notifications: Notification[]; + capabilities?: string[]; + auth?: UserAuth; +} + +export interface Notification { + title: string; + seen: boolean; +} + +export interface Setting { + id: number; + createdOn: number; + modifiedOn: number; + name: string; + value: any; +} + +// TODO: copy pasta not right +export interface Certificate { + id: number; + createdOn: number; + modifiedOn: number; + name: string; + acmeshServer: string; + caBundle: string; + maxDomains: number; + isWildcardSupported: boolean; + isSetup: boolean; +} + +export interface CertificateAuthority { + id: number; + createdOn: number; + modifiedOn: number; + name: string; + acmeshServer: string; + caBundle: string; + maxDomains: number; + isWildcardSupported: boolean; + isReadonly: boolean; +} + +export interface DNSProvider { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + name: string; + acmeshName: string; + dnsSleep: number; + meta: any; +} + +export interface DNSProvidersAcmeshField { + name: string; + type: string; + metaKey: string; + isRequired: boolean; + isSecret: boolean; +} + +export interface DNSProvidersAcmesh { + name: string; + acmeshName: string; + fields: DNSProvidersAcmeshField[]; +} + +export interface Host { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + type: string; + hostTemplateId: number; + listenInterface: number; + domainNames: string[]; + upstreamId: number; + certificateId: number; + accessListId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + paths: string; + upstreamOptions: string; + advancedConfig: string; + isDisabled: boolean; +} + +export interface HostTemplate { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + hostType: string; + template: string; +} diff --git a/frontend/src/api/npm/refreshToken.ts b/frontend/src/api/npm/refreshToken.ts new file mode 100644 index 00000000..de31f401 --- /dev/null +++ b/frontend/src/api/npm/refreshToken.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { TokenResponse } from "./responseTypes"; + +export async function refreshToken( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "/tokens", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts new file mode 100644 index 00000000..a15276af --- /dev/null +++ b/frontend/src/api/npm/responseTypes.ts @@ -0,0 +1,58 @@ +import { + Certificate, + CertificateAuthority, + DNSProvider, + Host, + HostTemplate, + Setting, + Sort, + User, +} from "./models"; + +export interface BaseResponse { + total: number; + offset: number; + limit: number; + sort: Sort[]; +} + +export interface HealthResponse { + commit: string; + errorReporting: boolean; + healthy: boolean; + setup: boolean; + version: string; +} + +export interface TokenResponse { + expires: number; + token: string; +} + +export interface SettingsResponse extends BaseResponse { + items: Setting[]; +} + +export interface CertificatesResponse extends BaseResponse { + items: Certificate[]; +} + +export interface CertificateAuthoritiesResponse extends BaseResponse { + items: CertificateAuthority[]; +} + +export interface UsersResponse extends BaseResponse { + items: User[]; +} + +export interface DNSProvidersResponse extends BaseResponse { + items: DNSProvider[]; +} + +export interface HostsResponse extends BaseResponse { + items: Host[]; +} + +export interface HostTemplatesResponse extends BaseResponse { + items: HostTemplate[]; +} diff --git a/frontend/src/api/npm/setAuth.ts b/frontend/src/api/npm/setAuth.ts new file mode 100644 index 00000000..f0a7049e --- /dev/null +++ b/frontend/src/api/npm/setAuth.ts @@ -0,0 +1,18 @@ +import * as api from "./base"; +import { UserAuth } from "./models"; + +export async function setAuth( + id: number | string = "me", + data: any, +): Promise { + const userId = id ? id : "me"; + if (data.id) { + delete data.id; + } + + const { result } = await api.post({ + url: `/users/${userId}/auth`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setCertificateAuthority.ts b/frontend/src/api/npm/setCertificateAuthority.ts new file mode 100644 index 00000000..1719e2ed --- /dev/null +++ b/frontend/src/api/npm/setCertificateAuthority.ts @@ -0,0 +1,17 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function setCertificateAuthority( + id: number, + data: any, +): Promise { + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/certificate-authorities/${id}`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setDNSProvider.ts b/frontend/src/api/npm/setDNSProvider.ts new file mode 100644 index 00000000..7eb23681 --- /dev/null +++ b/frontend/src/api/npm/setDNSProvider.ts @@ -0,0 +1,17 @@ +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function setDNSProvider( + id: number, + data: any, +): Promise { + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/dns-providers/${id}`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setUser.ts b/frontend/src/api/npm/setUser.ts new file mode 100644 index 00000000..743dba7a --- /dev/null +++ b/frontend/src/api/npm/setUser.ts @@ -0,0 +1,18 @@ +import * as api from "./base"; +import { User } from "./models"; + +export async function setUser( + id: number | string = "me", + data: any, +): Promise { + const userId = id ? id : "me"; + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/users/${userId}`, + data, + }); + return result; +} diff --git a/frontend/src/components/EmptyList.tsx b/frontend/src/components/EmptyList.tsx new file mode 100644 index 00000000..2f6a83b5 --- /dev/null +++ b/frontend/src/components/EmptyList.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from "react"; + +import { Box, Heading, Text } from "@chakra-ui/react"; + +interface EmptyListProps { + title: string; + summary: string; + createButton?: ReactNode; +} + +function EmptyList({ title, summary, createButton }: EmptyListProps) { + return ( + + + {title} + + {summary} + {createButton} + + ); +} + +export { EmptyList }; diff --git a/frontend/src/components/Flag/Flag.tsx b/frontend/src/components/Flag/Flag.tsx new file mode 100644 index 00000000..6f264f79 --- /dev/null +++ b/frontend/src/components/Flag/Flag.tsx @@ -0,0 +1,32 @@ +import { Box } from "@chakra-ui/layout"; +import { hasFlag } from "country-flag-icons"; +// @ts-ignore Creating a typing for a subfolder is not easily possible +import Flags from "country-flag-icons/react/3x2"; + +interface FlagProps { + /** + * Additional Class + */ + className?: string; + /** + * Two letter country code of flag + */ + countryCode: string; +} +function Flag({ className, countryCode }: FlagProps) { + countryCode = countryCode.toUpperCase(); + + if (hasFlag(countryCode)) { + // @ts-ignore have to do this because of above + const FlagElement = Flags[countryCode] as any; + return ( + + ); + } else { + console.error(`No flag for country ${countryCode} found!`); + + return ; + } +} + +export { Flag }; diff --git a/frontend/src/components/Flag/index.ts b/frontend/src/components/Flag/index.ts new file mode 100644 index 00000000..62845cc1 --- /dev/null +++ b/frontend/src/components/Flag/index.ts @@ -0,0 +1 @@ +export * from "./Flag"; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 00000000..97aecd97 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,64 @@ +import { + Box, + Container, + Link, + Stack, + Text, + Tooltip, + useColorModeValue, +} from "@chakra-ui/react"; +import { intl } from "locale"; + +function Footer() { + return ( + + + + {intl.formatMessage( + { id: "footer.copyright" }, + { year: new Date().getFullYear() }, + )} + + + + {intl.formatMessage({ id: "footer.userguide" })} + + + {intl.formatMessage({ id: "footer.changelog" })} + + + {intl.formatMessage({ id: "footer.github" })} + + + + v{process.env.REACT_APP_VERSION} + + + + + + ); +} + +export { Footer }; diff --git a/frontend/src/components/HelpDrawer/HelpDrawer.tsx b/frontend/src/components/HelpDrawer/HelpDrawer.tsx new file mode 100644 index 00000000..e6bf72c9 --- /dev/null +++ b/frontend/src/components/HelpDrawer/HelpDrawer.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; + +import { + Button, + Drawer, + DrawerContent, + DrawerOverlay, + DrawerBody, + useDisclosure, +} from "@chakra-ui/react"; +import { getLocale } from "locale"; +import { FiHelpCircle } from "react-icons/fi"; +import ReactMarkdown from "react-markdown"; + +import { getHelpFile } from "../../locale/src/HelpDoc"; + +interface HelpDrawerProps { + /** + * Section to show + */ + section: string; +} +function HelpDrawer({ section }: HelpDrawerProps) { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [markdownText, setMarkdownText] = useState(""); + const lang = getLocale(true); + + useEffect(() => { + try { + const docFile = getHelpFile(lang, section) as any; + fetch(docFile) + .then((response) => response.text()) + .then(setMarkdownText); + } catch (ex: any) { + setMarkdownText(`**ERROR:** ${ex.message}`); + } + }, [lang, section]); + + return ( + <> + + + + + + {markdownText} + + + + + ); +} + +export { HelpDrawer }; diff --git a/frontend/src/components/HelpDrawer/index.ts b/frontend/src/components/HelpDrawer/index.ts new file mode 100644 index 00000000..4b6357df --- /dev/null +++ b/frontend/src/components/HelpDrawer/index.ts @@ -0,0 +1 @@ +export * from "./HelpDrawer"; diff --git a/frontend/src/components/Loader/Loader.tsx b/frontend/src/components/Loader/Loader.tsx new file mode 100644 index 00000000..2c44bd6c --- /dev/null +++ b/frontend/src/components/Loader/Loader.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; + +import cn from "classnames"; + +interface LoaderProps { + /** + * Child elements within + */ + children?: ReactNode; + /** + * Additional Class + */ + className?: string; +} +function Loader({ children, className }: LoaderProps) { + return
{children}
; +} + +export { Loader }; diff --git a/frontend/src/components/Loader/index.ts b/frontend/src/components/Loader/index.ts new file mode 100644 index 00000000..f9f5a2b4 --- /dev/null +++ b/frontend/src/components/Loader/index.ts @@ -0,0 +1 @@ +export * from "./Loader"; diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx new file mode 100644 index 00000000..945237d1 --- /dev/null +++ b/frontend/src/components/Loading.tsx @@ -0,0 +1,12 @@ +import { Box } from "@chakra-ui/react"; +import { Loader } from "components"; + +function Loading() { + return ( + + + + ); +} + +export { Loading }; diff --git a/frontend/src/components/LocalePicker.tsx b/frontend/src/components/LocalePicker.tsx new file mode 100644 index 00000000..27ed3f1c --- /dev/null +++ b/frontend/src/components/LocalePicker.tsx @@ -0,0 +1,62 @@ +import { + Button, + Box, + Menu, + MenuButton, + MenuList, + MenuItem, +} from "@chakra-ui/react"; +import { Flag } from "components"; +import { useLocaleState } from "context"; +import { + changeLocale, + getFlagCodeForLocale, + intl, + localeOptions, +} from "locale"; + +interface LocalPickerProps { + /** + * On change handler + */ + onChange?: any; + /** + * Class + */ + className?: string; +} + +function LocalePicker({ onChange, className }: LocalPickerProps) { + const { locale, setLocale } = useLocaleState(); + + const changeTo = (lang: string) => { + changeLocale(lang); + setLocale(lang); + onChange && onChange(locale); + }; + + return ( + + + + + + + {localeOptions.map((item) => { + return ( + } + onClick={() => changeTo(item[0])} + // rel={item[1]} + key={`locale-${item[0]}`}> + {intl.formatMessage({ id: `locale-${item[1]}` })} + + ); + })} + + + + ); +} + +export { LocalePicker }; diff --git a/frontend/src/components/Navigation/Navigation.tsx b/frontend/src/components/Navigation/Navigation.tsx new file mode 100644 index 00000000..d8b7ac34 --- /dev/null +++ b/frontend/src/components/Navigation/Navigation.tsx @@ -0,0 +1,24 @@ +import { useDisclosure } from "@chakra-ui/react"; +import { NavigationHeader, NavigationMenu } from "components"; + +function Navigation() { + const { + isOpen: mobileNavIsOpen, + onToggle: mobileNavToggle, + onClose: mobileNavClose, + } = useDisclosure(); + return ( + <> + + + + ); +} + +export { Navigation }; diff --git a/frontend/src/components/Navigation/NavigationHeader.tsx b/frontend/src/components/Navigation/NavigationHeader.tsx new file mode 100644 index 00000000..f01b3de5 --- /dev/null +++ b/frontend/src/components/Navigation/NavigationHeader.tsx @@ -0,0 +1,113 @@ +import { + Avatar, + Box, + Button, + chakra, + Container, + Flex, + HStack, + Icon, + IconButton, + Menu, + MenuButton, + MenuDivider, + MenuItem, + MenuList, + Text, + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; +import { ThemeSwitcher } from "components"; +import { useAuthState } from "context"; +import { useUser } from "hooks"; +import { intl } from "locale"; +import { ChangePasswordModal, ProfileModal } from "modals"; +import { FiLock, FiLogOut, FiMenu, FiUser, FiX } from "react-icons/fi"; + +interface NavigationHeaderProps { + mobileNavIsOpen: boolean; + toggleMobileNav: () => void; +} +function NavigationHeader({ + mobileNavIsOpen, + toggleMobileNav, +}: NavigationHeaderProps) { + const passwordDisclosure = useDisclosure(); + const profileDisclosure = useDisclosure(); + const { data: user } = useUser("me"); + const { logout } = useAuthState(); + + return ( + + + + } + /> + + + + {intl.formatMessage({ id: "brand.name" })} + + + + + + + + + + + } + onClick={profileDisclosure.onOpen}> + {intl.formatMessage({ id: "profile.title" })} + + } + onClick={passwordDisclosure.onOpen}> + {intl.formatMessage({ id: "change-password" })} + + + }> + {intl.formatMessage({ id: "profile.logout" })} + + + + + + + + + + + ); +} + +export { NavigationHeader }; diff --git a/frontend/src/components/Navigation/NavigationMenu.tsx b/frontend/src/components/Navigation/NavigationMenu.tsx new file mode 100644 index 00000000..bc7575e5 --- /dev/null +++ b/frontend/src/components/Navigation/NavigationMenu.tsx @@ -0,0 +1,331 @@ +import { FC, useCallback, useMemo, ReactNode } from "react"; + +import { + Box, + Collapse, + Flex, + forwardRef, + HStack, + Icon, + Link, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + Stack, + useColorModeValue, + useDisclosure, + Container, + useBreakpointValue, +} from "@chakra-ui/react"; +import { intl } from "locale"; +import { + FiHome, + FiSettings, + FiUser, + FiBook, + FiLock, + FiShield, + FiMonitor, + FiChevronDown, +} from "react-icons/fi"; +import { Link as RouterLink, useLocation } from "react-router-dom"; + +interface NavItem { + /** Displayed label */ + label: string; + /** Icon shown before the label */ + icon: ReactNode; + /** Link where to navigate to */ + to?: string; + subItems?: { label: string; to: string }[]; +} + +const navItems: NavItem[] = [ + { + label: intl.formatMessage({ id: "dashboard.title" }), + icon: , + to: "/", + }, + { + label: intl.formatMessage({ id: "hosts.title" }), + icon: , + to: "/hosts", + }, + { + label: intl.formatMessage({ id: "access-lists.title" }), + icon: , + to: "/access-lists", + }, + { + label: intl.formatMessage({ id: "ssl.title" }), + icon: , + subItems: [ + { + label: intl.formatMessage({ id: "certificates.title" }), + to: "/ssl/certificates", + }, + { + label: intl.formatMessage({ id: "certificate-authorities.title" }), + to: "/ssl/authorities", + }, + { + label: intl.formatMessage({ id: "dns-providers.title" }), + to: "/ssl/dns-providers", + }, + ], + }, + { + label: intl.formatMessage({ id: "audit-log.title" }), + icon: , + to: "/audit-log", + }, + { + label: intl.formatMessage({ id: "users.title" }), + icon: , + to: "/users", + }, + { + label: intl.formatMessage({ id: "settings.title" }), + icon: , + subItems: [ + { + label: intl.formatMessage({ id: "general-settings.title" }), + to: "/settings/general", + }, + { + label: intl.formatMessage({ id: "host-templates.title" }), + to: "/settings/host-templates", + }, + ], + }, +]; + +interface NavigationMenuProps { + /** Navigation is currently hidden on mobile */ + mobileNavIsOpen: boolean; + closeMobileNav: () => void; +} +function NavigationMenu({ + mobileNavIsOpen, + closeMobileNav, +}: NavigationMenuProps) { + const isMobile = useBreakpointValue({ base: true, md: false }); + return ( + <> + {isMobile ? ( + + + + ) : ( + + )} + + ); +} + +/** Single tab element for desktop navigation */ +type NavTabProps = Omit & { active?: boolean }; +const NavTab = forwardRef( + ({ label, icon, to, active, ...props }, ref) => { + const linkColor = useColorModeValue("gray.500", "gray.200"); + const linkHoverColor = useColorModeValue("gray.900", "white"); + return ( + + {icon} + + {label} + + + ); + }, +); + +const DesktopNavigation: FC = () => { + const path = useLocation().pathname; + const activeNavItemIndex = useMemo( + () => + navItems.findIndex((item) => { + // Find the nav item whose location / sub items location is the beginning of the currently active path + if (item.to) { + // console.debug(item.to, path); + if (item.to === "/") { + return path === item.to; + } + return path.startsWith(item.to !== "" ? item.to : "/dashboard"); + } else if (item.subItems) { + return item.subItems.some((subItem) => path.startsWith(subItem.to)); + } + return false; + }), + [path], + ); + + return ( + + + + {navItems.map((navItem, index) => { + const { subItems, ...propsWithoutSubItems } = navItem; + const additionalProps: Partial = {}; + if (index === activeNavItemIndex) { + additionalProps["active"] = true; + } + if (subItems) { + return ( + + + {subItems && ( + + {subItems.map((item, subIndex) => ( + + {item.label} + + ))} + + )} + + ); + } else { + return ( + + ); + } + })} + + + + ); +}; + +const MobileNavigation: FC> = ({ + closeMobileNav, +}) => { + return ( + + {navItems.map((navItem, index) => ( + + ))} + + ); +}; + +const MobileNavItem: FC< + NavItem & { + index: number; + closeMobileNav: NavigationMenuProps["closeMobileNav"]; + } +> = ({ closeMobileNav, ...props }) => { + const { isOpen, onToggle } = useDisclosure(); + + const onClickHandler = useCallback(() => { + if (props.subItems) { + // Toggle accordeon + onToggle(); + } else { + // Close menu on navigate + closeMobileNav(); + } + }, [closeMobileNav, onToggle, props.subItems]); + + return ( + + + + + {props.icon} + + {props.label} + + + {props.subItems && ( + + )} + + + + + {props.subItems && + props.subItems.map((subItem, subIndex) => ( + + {subItem.label} + + ))} + + + + + ); +}; + +export { NavigationMenu }; diff --git a/frontend/src/components/Navigation/index.ts b/frontend/src/components/Navigation/index.ts new file mode 100644 index 00000000..302ad0e6 --- /dev/null +++ b/frontend/src/components/Navigation/index.ts @@ -0,0 +1,3 @@ +export * from "./Navigation"; +export * from "./NavigationHeader"; +export * from "./NavigationMenu"; diff --git a/frontend/src/components/Permissions/AdminPermissionSelector.tsx b/frontend/src/components/Permissions/AdminPermissionSelector.tsx new file mode 100644 index 00000000..915636a2 --- /dev/null +++ b/frontend/src/components/Permissions/AdminPermissionSelector.tsx @@ -0,0 +1,36 @@ +import { MouseEventHandler } from "react"; + +import { Heading, Stack, Text, useColorModeValue } from "@chakra-ui/react"; +import { intl } from "locale"; + +interface AdminPermissionSelectorProps { + selected?: boolean; + onClick: MouseEventHandler; +} + +function AdminPermissionSelector({ + selected, + onClick, +}: AdminPermissionSelectorProps) { + return ( + + + {intl.formatMessage({ id: "full-access" })} + + + {intl.formatMessage({ id: "full-access.description" })} + + + ); +} + +export { AdminPermissionSelector }; diff --git a/frontend/src/components/Permissions/PermissionSelector.tsx b/frontend/src/components/Permissions/PermissionSelector.tsx new file mode 100644 index 00000000..146c7822 --- /dev/null +++ b/frontend/src/components/Permissions/PermissionSelector.tsx @@ -0,0 +1,285 @@ +import { ChangeEvent, MouseEventHandler } from "react"; + +import { + Flex, + Heading, + Select, + Stack, + Text, + useColorModeValue, +} from "@chakra-ui/react"; +import { intl } from "locale"; + +interface PermissionSelectorProps { + capabilities: string[]; + selected?: boolean; + onClick: MouseEventHandler; + onChange: (i: string[]) => any; +} + +function PermissionSelector({ + capabilities, + selected, + onClick, + onChange, +}: PermissionSelectorProps) { + const textColor = useColorModeValue("gray.700", "gray.400"); + + const onSelectChange = ({ target }: ChangeEvent) => { + // remove all items starting with target.name + const i: string[] = []; + const re = new RegExp(`^${target.name}\\.`, "g"); + capabilities.forEach((capability) => { + if (!capability.match(re)) { + i.push(capability); + } + }); + + // add a new item, if value is something, and doesn't already exist + if (target.value) { + const c = `${target.name}.${target.value}`; + if (i.indexOf(c) === -1) { + i.push(c); + } + } + + onChange(i); + }; + + const getDefaultValue = (c: string): string => { + if (capabilities.indexOf(`${c}.manage`) !== -1) { + return "manage"; + } + if (capabilities.indexOf(`${c}.view`) !== -1) { + return "view"; + } + return ""; + }; + + return ( + + + {intl.formatMessage({ id: "restricted-access" })} + + {selected ? ( + + + + {intl.formatMessage({ id: "access-lists.title" })} + + + + + + + + {intl.formatMessage({ id: "audit-log.title" })} + + + + + + + + {intl.formatMessage({ id: "certificates.title" })} + + + + + + + + {intl.formatMessage({ id: "certificate-authorities.title" })} + + + + + + + + {intl.formatMessage({ id: "dns-providers.title" })} + + + + + + + {intl.formatMessage({ id: "hosts.title" })} + + + + + + + {intl.formatMessage({ id: "host-templates.title" })} + + + + + + + {intl.formatMessage({ id: "settings.title" })} + + + + + + {intl.formatMessage({ id: "users.title" })} + + + + + + ) : ( + + {intl.formatMessage({ id: "restricted-access.description" })} + + )} + + ); +} + +export { PermissionSelector }; diff --git a/frontend/src/components/Permissions/index.ts b/frontend/src/components/Permissions/index.ts new file mode 100644 index 00000000..e0cf8120 --- /dev/null +++ b/frontend/src/components/Permissions/index.ts @@ -0,0 +1,2 @@ +export * from "./AdminPermissionSelector"; +export * from "./PermissionSelector"; diff --git a/frontend/src/components/PrettyButton.tsx b/frontend/src/components/PrettyButton.tsx new file mode 100644 index 00000000..b717d0f7 --- /dev/null +++ b/frontend/src/components/PrettyButton.tsx @@ -0,0 +1,20 @@ +import { Button, ButtonProps } from "@chakra-ui/react"; + +function PrettyButton(props: ButtonProps) { + return ( + + ); +} + +export { PrettyButton }; diff --git a/frontend/src/components/SiteWrapper.tsx b/frontend/src/components/SiteWrapper.tsx new file mode 100644 index 00000000..d451934c --- /dev/null +++ b/frontend/src/components/SiteWrapper.tsx @@ -0,0 +1,32 @@ +import { ReactNode } from "react"; + +import { Box, Container } from "@chakra-ui/react"; +import { Footer, Navigation } from "components"; + +interface Props { + children?: ReactNode; +} +function SiteWrapper({ children }: Props) { + return ( + + + + + + + {children} + + + +