From 18ae902a0459ac0ee25f1e25e658ba34ad0a6ca3 Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 20 Apr 2024 17:27:10 +0200 Subject: [PATCH] remove global/frintend and backend folder --- backend/.gitignore | 8 - backend/.prettierrc | 7 - backend/app.js | 87 - backend/db.js | 28 - backend/doc/api.swagger.json | 1456 ----------------- backend/eslint.config.mjs | 5 - backend/index.js | 49 - backend/internal/access-list.js | 511 ------ backend/internal/audit-log.js | 71 - backend/internal/certificate.js | 1125 ------------- backend/internal/dead-host.js | 450 ----- backend/internal/host.js | 217 --- backend/internal/ip_ranges.js | 150 -- backend/internal/nginx.js | 376 ----- backend/internal/proxy-host.js | 457 ------ backend/internal/redirection-host.js | 450 ----- backend/internal/report.js | 32 - backend/internal/setting.js | 125 -- backend/internal/stream.js | 331 ---- backend/internal/token.js | 155 -- backend/internal/user.js | 482 ------ backend/knexfile.js | 19 - backend/lib/access.js | 300 ---- 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/certbot.js | 75 - backend/lib/config.js | 186 --- backend/lib/error.js | 98 -- backend/lib/express/cors.js | 36 - backend/lib/express/jwt-decode.js | 15 - backend/lib/express/jwt.js | 13 - backend/lib/express/pagination.js | 53 - backend/lib/express/user-id-from-me.js | 9 - backend/lib/helpers.js | 30 - backend/lib/migrate_template.js | 54 - backend/lib/utils.js | 138 -- backend/lib/validator/api.js | 43 - backend/lib/validator/index.js | 43 - backend/logger.js | 14 - backend/migrate.js | 14 - backend/migrations/20180618015850_initial.js | 205 --- .../migrations/20180929054513_websockets.js | 35 - .../migrations/20181019052346_forward_host.js | 35 - .../20181113041458_http2_support.js | 49 - .../20181213013211_forward_scheme.js | 35 - backend/migrations/20190104035154_disabled.js | 56 - .../20190215115310_customlocations.js | 36 - backend/migrations/20190218060101_hsts.js | 52 - backend/migrations/20190227065017_settings.js | 39 - .../20200410143839_access_list_client.js | 51 - .../20200410143840_access_list_client_fix.js | 35 - .../migrations/20201014143841_pass_auth.js | 42 - .../20210210154702_redirection_scheme.js | 42 - .../20210210154703_redirection_status_code.js | 42 - .../20210423103500_stream_domain.js | 42 - .../20211108145214_regenerate_default_host.js | 51 - backend/models/access_list.js | 86 - backend/models/access_list_auth.js | 54 - backend/models/access_list_client.js | 54 - backend/models/audit-log.js | 52 - backend/models/auth.js | 82 - backend/models/certificate.js | 65 - backend/models/dead_host.js | 72 - backend/models/now_helper.js | 12 - backend/models/proxy_host.js | 84 - backend/models/redirection_host.js | 74 - backend/models/setting.js | 30 - backend/models/stream.js | 55 - backend/models/token.js | 126 -- backend/models/user.js | 52 - backend/models/user_permission.js | 29 - backend/nodemon.json | 7 - backend/package.json | 39 - backend/password-reset.js | 59 - backend/routes/api/audit-log.js | 54 - backend/routes/api/main.js | 51 - backend/routes/api/nginx/access_lists.js | 150 -- backend/routes/api/nginx/certificates.js | 299 ---- backend/routes/api/nginx/dead_hosts.js | 198 --- backend/routes/api/nginx/proxy_hosts.js | 198 --- backend/routes/api/nginx/redirection_hosts.js | 198 --- backend/routes/api/nginx/streams.js | 198 --- backend/routes/api/reports.js | 29 - backend/routes/api/schema.js | 36 - backend/routes/api/settings.js | 97 -- backend/routes/api/tokens.js | 53 - 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/setup.js | 145 -- backend/sqlite-vaccum.js | 23 - backend/templates/_access.conf | 25 - backend/templates/_brotli.conf | 4 - backend/templates/_certificates.conf | 15 - backend/templates/_forced_tls.conf | 6 - backend/templates/_header_comment.conf | 3 - backend/templates/_hsts.conf | 17 - backend/templates/_listen.conf | 20 - backend/templates/_location.conf | 17 - backend/templates/dead_host.conf | 26 - backend/templates/default.conf | 61 - backend/templates/ip_ranges.conf | 3 - backend/templates/proxy_host.conf | 58 - backend/templates/redirection_host.conf | 28 - backend/templates/stream.conf | 29 - darkmode.css | 247 --- frontend/.gitignore | 4 - frontend/app-images/default-avatar.jpg | Bin 1753 -> 0 bytes .../favicons/android-chrome-192x192.png | Bin 17104 -> 0 bytes .../favicons/android-chrome-512x512.png | Bin 54834 -> 0 bytes .../app-images/favicons/apple-touch-icon.png | Bin 15800 -> 0 bytes .../app-images/favicons/browserconfig.xml | 9 - .../app-images/favicons/favicon-16x16.png | Bin 1220 -> 0 bytes .../app-images/favicons/favicon-32x32.png | Bin 2386 -> 0 bytes frontend/app-images/favicons/favicon.ico | Bin 15086 -> 0 bytes .../app-images/favicons/mstile-150x150.png | Bin 11672 -> 0 bytes .../app-images/favicons/safari-pinned-tab.svg | 1 - frontend/app-images/favicons/site.webmanifest | 19 - frontend/app-images/logo-256.png | Bin 29709 -> 0 bytes .../app-images/logo-text-vertical-grey.png | Bin 15526 -> 0 bytes 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/html/index.ejs | 9 - frontend/html/login.ejs | 9 - frontend/html/partials/footer.ejs | 2 - frontend/html/partials/header.ejs | 73 - frontend/images | 1 - 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 | 184 --- frontend/js/app/nginx/certificates/form.js | 297 ---- .../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 | 274 ---- 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 | 357 ---- 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 | 255 --- frontend/js/app/nginx/redirection/form.js | 276 ---- .../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 | 57 - 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 | 18 - 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 | 297 ---- 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 | 46 - 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/webpack.config.js | 143 -- global/README.md | 19 - global/certbot-dns-plugins.json | 338 ---- 339 files changed, 25751 deletions(-) delete mode 100644 backend/.gitignore delete mode 100644 backend/.prettierrc delete mode 100644 backend/app.js delete mode 100644 backend/db.js delete mode 100644 backend/doc/api.swagger.json delete mode 100644 backend/eslint.config.mjs delete mode 100755 backend/index.js delete mode 100644 backend/internal/access-list.js delete mode 100644 backend/internal/audit-log.js delete mode 100644 backend/internal/certificate.js delete mode 100644 backend/internal/dead-host.js delete mode 100644 backend/internal/host.js delete mode 100644 backend/internal/ip_ranges.js delete mode 100644 backend/internal/nginx.js 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 delete mode 100644 backend/internal/stream.js delete mode 100644 backend/internal/token.js delete mode 100644 backend/internal/user.js 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/certbot.js delete mode 100644 backend/lib/config.js 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 100755 backend/password-reset.js 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 delete mode 100644 backend/setup.js delete mode 100755 backend/sqlite-vaccum.js delete mode 100644 backend/templates/_access.conf delete mode 100644 backend/templates/_brotli.conf delete mode 100644 backend/templates/_certificates.conf delete mode 100644 backend/templates/_forced_tls.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/proxy_host.conf delete mode 100644 backend/templates/redirection_host.conf delete mode 100644 backend/templates/stream.conf delete mode 100644 darkmode.css delete mode 100644 frontend/.gitignore delete mode 100644 frontend/app-images/default-avatar.jpg delete mode 100644 frontend/app-images/favicons/android-chrome-192x192.png delete mode 100644 frontend/app-images/favicons/android-chrome-512x512.png delete mode 100644 frontend/app-images/favicons/apple-touch-icon.png delete mode 100644 frontend/app-images/favicons/browserconfig.xml delete mode 100644 frontend/app-images/favicons/favicon-16x16.png delete mode 100644 frontend/app-images/favicons/favicon-32x32.png delete mode 100644 frontend/app-images/favicons/favicon.ico delete mode 100644 frontend/app-images/favicons/mstile-150x150.png delete mode 100644 frontend/app-images/favicons/safari-pinned-tab.svg delete mode 100644 frontend/app-images/favicons/site.webmanifest delete mode 100644 frontend/app-images/logo-256.png delete mode 100644 frontend/app-images/logo-text-vertical-grey.png 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 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 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 delete mode 100644 frontend/package.json 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 delete mode 100644 frontend/webpack.config.js delete mode 100644 global/README.md delete mode 100644 global/certbot-dns-plugins.json 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/.prettierrc b/backend/.prettierrc deleted file mode 100644 index 6a534db7..00000000 --- a/backend/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "useTabs": true, - "printWidth": 1000, - "singleQuote": true, - "bracketSameLine": true -} diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index e2c1d5b5..00000000 --- a/backend/app.js +++ /dev/null @@ -1,87 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const fileUpload = require('express-fileupload'); -const compression = require('compression'); -const config = require('./lib/config'); -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 (config.debug()) { - 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) { - const payload = { - error: { - code: err.status, - message: err.public ? err.message : 'Internal Error', - }, - }; - - if (config.debug() || (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 (config.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/db.js b/backend/db.js deleted file mode 100644 index c3fec082..00000000 --- a/backend/db.js +++ /dev/null @@ -1,28 +0,0 @@ -const config = require('./lib/config'); - -if (!config.has('database')) { - throw new Error('Database config does not exist! Please read the instructions: https://github.com/ZoeyVid/NPMplus'); -} - -function generateDbConfig() { - const cfg = config.get('database'); - if (cfg.engine === 'knex-native') { - return cfg.knex; - } - return { - client: cfg.engine, - connection: { - host: cfg.host, - user: cfg.user, - password: cfg.password, - database: cfg.name, - port: cfg.port, - ssl: cfg.tls, - }, - migrations: { - tableName: 'migrations', - }, - }; -} - -module.exports = require('knex')(generateDbConfig()); diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json deleted file mode 100644 index 4d97d5e0..00000000 --- a/backend/doc/api.swagger.json +++ /dev/null @@ -1,1456 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "NPMplus API", - "version": "2.x.x" - }, - "servers": [ - { - "url": "https://127.0.0.1:81/api" - } - ], - "paths": { - "/": { - "get": { - "operationId": "health", - "summary": "Returns the API health status", - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "status": "OK", - "version": { - "major": 2, - "minor": 1, - "revision": 0 - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/HealthObject" - } - } - } - } - } - } - }, - "/nginx/proxy-hosts": { - "get": { - "operationId": "getProxyHosts", - "summary": "Get all proxy hosts", - "tags": ["Proxy Hosts"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "query", - "name": "expand", - "description": "Expansions", - "schema": { - "type": "string", - "enum": ["access_list", "owner", "certificate"] - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": 1, - "created_on": "2023-03-30T01:12:23.000Z", - "modified_on": "2023-03-30T02:15:40.000Z", - "owner_user_id": 1, - "domain_names": ["aasdasdad"], - "forward_host": "asdasd", - "forward_port": 80, - "access_list_id": 0, - "certificate_id": 0, - "ssl_forced": 0, - "caching_enabled": 0, - "block_exploits": 0, - "advanced_config": "sdfsdfsdf", - "meta": { - "letsencrypt_agree": false, - "dns_challenge": false, - "nginx_online": false, - "nginx_err": "Command failed: nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /urs/local/nginx/conf/nginx.conf test failed\n" - }, - "allow_websocket_upgrade": 0, - "http2_support": 0, - "forward_scheme": "http", - "enabled": 1, - "locations": [], - "hsts_enabled": 0, - "hsts_subdomains": 0, - "owner": { - "id": 1, - "created_on": "2023-03-30T01:11:50.000Z", - "modified_on": "2023-03-30T01:11:50.000Z", - "is_deleted": 0, - "is_disabled": 0, - "email": "admin@example.com", - "name": "Administrator", - "nickname": "Admin", - "avatar": "", - "roles": ["admin"] - }, - "access_list": null, - "certificate": null - }, - { - "id": 2, - "created_on": "2023-03-30T02:11:49.000Z", - "modified_on": "2023-03-30T02:11:49.000Z", - "owner_user_id": 1, - "domain_names": ["test.example.com"], - "forward_host": "1.1.1.1", - "forward_port": 80, - "access_list_id": 0, - "certificate_id": 0, - "ssl_forced": 0, - "caching_enabled": 0, - "block_exploits": 0, - "advanced_config": "", - "meta": { - "letsencrypt_agree": false, - "dns_challenge": false, - "nginx_online": true, - "nginx_err": null - }, - "allow_websocket_upgrade": 0, - "http2_support": 0, - "forward_scheme": "http", - "enabled": 1, - "locations": [], - "hsts_enabled": 0, - "hsts_subdomains": 0, - "owner": { - "id": 1, - "created_on": "2023-03-30T01:11:50.000Z", - "modified_on": "2023-03-30T01:11:50.000Z", - "is_deleted": 0, - "is_disabled": 0, - "email": "admin@example.com", - "name": "Administrator", - "nickname": "Admin", - "avatar": "", - "roles": ["admin"] - }, - "access_list": null, - "certificate": null - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/ProxyHostsList" - } - } - } - } - } - }, - "post": { - "operationId": "createProxyHost", - "summary": "Create a Proxy Host", - "tags": ["Proxy Hosts"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "body", - "name": "proxyhost", - "description": "Proxy Host Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/ProxyHostObject" - } - } - ], - "responses": { - "201": { - "description": "201 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 3, - "created_on": "2023-03-30T02:31:27.000Z", - "modified_on": "2023-03-30T02:31:27.000Z", - "owner_user_id": 1, - "domain_names": ["test2.example.com"], - "forward_host": "1.1.1.1", - "forward_port": 80, - "access_list_id": 0, - "certificate_id": 0, - "ssl_forced": 0, - "caching_enabled": 0, - "block_exploits": 0, - "advanced_config": "", - "meta": { - "letsencrypt_agree": false, - "dns_challenge": false - }, - "allow_websocket_upgrade": 0, - "http2_support": 0, - "forward_scheme": "http", - "enabled": 1, - "locations": [], - "hsts_enabled": 0, - "hsts_subdomains": 0, - "certificate": null, - "owner": { - "id": 1, - "created_on": "2023-03-30T01:11:50.000Z", - "modified_on": "2023-03-30T01:11:50.000Z", - "is_deleted": 0, - "is_disabled": 0, - "email": "admin@example.com", - "name": "Administrator", - "nickname": "Admin", - "avatar": "", - "roles": ["admin"] - }, - "access_list": null, - "use_default_location": true, - "ipv6": true - } - } - }, - "schema": { - "$ref": "#/components/schemas/ProxyHostObject" - } - } - } - } - } - } - }, - "/schema": { - "get": { - "operationId": "schema", - "responses": { - "200": { - "description": "200 response" - } - }, - "summary": "Returns this swagger API schema" - } - }, - "/tokens": { - "get": { - "operationId": "refreshToken", - "summary": "Refresh your access token", - "tags": ["Tokens"], - "security": [ - { - "BearerAuth": ["tokens"] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - } - } - } - }, - "post": { - "operationId": "requestToken", - "parameters": [ - { - "description": "Credentials Payload", - "in": "body", - "name": "credentials", - "required": true, - "schema": { - "additionalProperties": false, - "properties": { - "identity": { - "minLength": 1, - "type": "string" - }, - "scope": { - "minLength": 1, - "type": "string", - "enum": ["user"] - }, - "secret": { - "minLength": 1, - "type": "string" - } - }, - "required": ["identity", "secret"], - "type": "object" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "result": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - }, - "description": "200 response" - } - }, - "summary": "Request a new access token from credentials", - "tags": ["Tokens"] - } - }, - "/settings": { - "get": { - "operationId": "getSettings", - "summary": "Get all settings", - "tags": ["Settings"], - "security": [ - { - "BearerAuth": ["settings"] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/SettingsList" - } - } - } - } - } - } - }, - "/settings/{settingID}": { - "get": { - "operationId": "getSetting", - "summary": "Get a setting", - "tags": ["Settings"], - "security": [ - { - "BearerAuth": ["settings"] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } - }, - "put": { - "operationId": "updateSetting", - "summary": "Update a setting", - "tags": ["Settings"], - "security": [ - { - "BearerAuth": ["settings"] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - }, - { - "in": "body", - "name": "setting", - "description": "Setting Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } - } - }, - "/users": { - "get": { - "operationId": "getUsers", - "summary": "Get all users", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "query", - "name": "expand", - "description": "Expansions", - "schema": { - "type": "string", - "enum": ["permissions"] - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": ["admin"] - } - ] - }, - "withPermissions": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": ["admin"], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/UsersList" - } - } - } - } - } - }, - "post": { - "operationId": "createUser", - "summary": "Create a User", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "201": { - "description": "201 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": ["admin"], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - } - }, - "/users/{userID}": { - "get": { - "operationId": "getUser", - "summary": "Get a user", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 1 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": ["admin"] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - }, - "put": { - "operationId": "updateUser", - "summary": "Update a User", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": ["admin"] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - }, - "delete": { - "operationId": "deleteUser", - "summary": "Delete a User", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/auth": { - "put": { - "operationId": "updateUserAuth", - "summary": "Update a User's Authentication", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/AuthObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/permissions": { - "put": { - "operationId": "updateUserPermissions", - "summary": "Update a User's Permissions", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "Permissions Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/PermissionsObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/login": { - "put": { - "operationId": "loginAsUser", - "summary": "Login as this user", - "tags": ["Users"], - "security": [ - { - "BearerAuth": ["users"] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg", - "expires": "2020-01-31T10:56:23.239Z", - "user": { - "id": 1, - "created_on": "2020-01-30T10:43:44.000Z", - "modified_on": "2020-01-30T10:43:44.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", - "roles": ["admin"] - } - } - } - }, - "schema": { - "type": "object", - "description": "Login object", - "required": ["expires", "token", "user"], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - }, - "user": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - } - } - } - }, - "/reports/hosts": { - "get": { - "operationId": "reportsHosts", - "summary": "Report on Host Statistics", - "tags": ["Reports"], - "security": [ - { - "BearerAuth": ["reports"] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } - } - }, - "/audit-log": { - "get": { - "operationId": "getAuditLog", - "summary": "Get Audit Log", - "tags": ["Audit Log"], - "security": [ - { - "BearerAuth": ["audit-log"] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } - } - } - }, - "components": { - "securitySchemes": { - "BearerAuth": { - "type": "http", - "scheme": "bearer" - } - }, - "schemas": { - "HealthObject": { - "type": "object", - "description": "Health object", - "additionalProperties": false, - "required": ["status", "version"], - "properties": { - "status": { - "type": "string", - "description": "Healthy", - "example": "OK" - }, - "version": { - "type": "object", - "description": "The version object", - "example": { - "major": 2, - "minor": 0, - "revision": 0 - }, - "additionalProperties": false, - "required": ["major", "minor", "revision"], - "properties": { - "major": { - "type": "integer", - "minimum": 0 - }, - "minor": { - "type": "integer", - "minimum": 0 - }, - "revision": { - "type": "integer", - "minimum": 0 - } - } - } - } - }, - "TokenObject": { - "type": "object", - "description": "Token object", - "required": ["expires", "token"], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - } - } - }, - "ProxyHostObject": { - "type": "object", - "description": "Proxy Host object", - "required": [ - "id", - "created_on", - "modified_on", - "owner_user_id", - "domain_names", - "forward_host", - "forward_port", - "access_list_id", - "certificate_id", - "ssl_forced", - "caching_enabled", - "block_exploits", - "advanced_config", - "meta", - "allow_websocket_upgrade", - "http2_support", - "forward_scheme", - "enabled", - "locations", - "hsts_enabled", - "hsts_subdomains", - "certificate", - "use_default_location", - "ipv6" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": "integer", - "description": "Proxy Host ID", - "minimum": 1, - "example": 1 - }, - "created_on": { - "type": "string", - "description": "Created Date", - "example": "2020-01-30T09:36:08.000Z" - }, - "modified_on": { - "type": "string", - "description": "Modified Date", - "example": "2020-01-30T09:41:04.000Z" - }, - "owner_user_id": { - "type": "integer", - "minimum": 1, - "example": 1 - }, - "domain_names": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "minLength": 1 - } - }, - "forward_host": { - "type": "string", - "minLength": 1 - }, - "forward_port": { - "type": "integer", - "minimum": 1 - }, - "access_list_id": { - "type": "integer" - }, - "certificate_id": { - "type": "integer" - }, - "ssl_forced": { - "type": "integer" - }, - "caching_enabled": { - "type": "integer" - }, - "block_exploits": { - "type": "integer" - }, - "advanced_config": { - "type": "string" - }, - "meta": { - "type": "object" - }, - "allow_websocket_upgrade": { - "type": "integer" - }, - "http2_support": { - "type": "integer" - }, - "forward_scheme": { - "type": "string" - }, - "enabled": { - "type": "integer" - }, - "locations": { - "type": "array" - }, - "hsts_enabled": { - "type": "integer" - }, - "hsts_subdomains": { - "type": "integer" - }, - "certificate": { - "type": "object", - "nullable": true - }, - "owner": { - "type": "object", - "nullable": true - }, - "access_list": { - "type": "object", - "nullable": true - }, - "use_default_location": { - "type": "boolean" - }, - "ipv6": { - "type": "boolean" - } - } - }, - "ProxyHostsList": { - "type": "array", - "description": "Proxyn Hosts list", - "items": { - "$ref": "#/components/schemas/ProxyHostObject" - } - }, - "SettingObject": { - "type": "object", - "description": "Setting object", - "required": ["id", "name", "description", "value", "meta"], - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "Setting ID", - "minLength": 1, - "example": "default-site" - }, - "name": { - "type": "string", - "description": "Setting Display Name", - "minLength": 1, - "example": "Default Site" - }, - "description": { - "type": "string", - "description": "Meaningful description", - "minLength": 1, - "example": "What to show when Nginx is hit with an unknown Host" - }, - "value": { - "description": "Value in almost any form", - "example": "congratulations", - "oneOf": [ - { - "type": "string", - "minLength": 1 - }, - { - "type": "integer" - }, - { - "type": "object" - }, - { - "type": "number" - }, - { - "type": "array" - } - ] - }, - "meta": { - "description": "Extra metadata", - "example": {}, - "type": "object" - } - } - }, - "SettingsList": { - "type": "array", - "description": "Setting list", - "items": { - "$ref": "#/components/schemas/SettingObject" - } - }, - "UserObject": { - "type": "object", - "description": "User object", - "required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"], - "additionalProperties": false, - "properties": { - "id": { - "type": "integer", - "description": "User ID", - "minimum": 1, - "example": 1 - }, - "created_on": { - "type": "string", - "description": "Created Date", - "example": "2020-01-30T09:36:08.000Z" - }, - "modified_on": { - "type": "string", - "description": "Modified Date", - "example": "2020-01-30T09:41:04.000Z" - }, - "is_disabled": { - "type": "integer", - "minimum": 0, - "maximum": 1, - "description": "Is user Disabled (0 = false, 1 = true)", - "example": 0 - }, - "email": { - "type": "string", - "description": "Email", - "minLength": 3, - "example": "jc@jc21.com" - }, - "name": { - "type": "string", - "description": "Name", - "minLength": 1, - "example": "Jamie Curnow" - }, - "nickname": { - "type": "string", - "description": "Nickname", - "example": "James" - }, - "avatar": { - "type": "string", - "description": "Gravatar URL based on email, without scheme", - "example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm" - }, - "roles": { - "description": "Roles applied", - "example": ["admin"], - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UsersList": { - "type": "array", - "description": "User list", - "items": { - "$ref": "#/components/schemas/UserObject" - } - }, - "AuthObject": { - "type": "object", - "description": "Authentication Object", - "required": ["type", "secret"], - "properties": { - "type": { - "type": "string", - "pattern": "^password$", - "example": "password" - }, - "current": { - "type": "string", - "minLength": 1, - "maxLength": 99, - "example": "iArhP1j7p1P6TA92FA2FMbbUGYqwcYzxC4AVEe12Wbi94FY9gNN62aKyF1shrvG4NycjjX9KfmDQiwkLZH1ZDR9xMjiG2QmoHXi" - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 99, - "example": "5wdvvveVKkNNr8K7fSQKoUWbYyCZ2abtLaa1J5LzAvMfkGVcGBXHQ32iuPdeKdNfQVZiPKee3ZPKaGMvFR5t94QCeZbK3faSVYu" - } - } - }, - "PermissionsObject": { - "type": "object", - "properties": { - "visibility": { - "type": "string", - "description": "Visibility Type", - "enum": ["all", "user"] - }, - "access_lists": { - "type": "string", - "description": "Access Lists Permissions", - "enum": ["hidden", "view", "manage"] - }, - "dead_hosts": { - "type": "string", - "description": "404 Hosts Permissions", - "enum": ["hidden", "view", "manage"] - }, - "proxy_hosts": { - "type": "string", - "description": "Proxy Hosts Permissions", - "enum": ["hidden", "view", "manage"] - }, - "redirection_hosts": { - "type": "string", - "description": "Redirection Permissions", - "enum": ["hidden", "view", "manage"] - }, - "streams": { - "type": "string", - "description": "Streams Permissions", - "enum": ["hidden", "view", "manage"] - }, - "certificates": { - "type": "string", - "description": "Certificates Permissions", - "enum": ["hidden", "view", "manage"] - } - } - }, - "HostReportObject": { - "type": "object", - "properties": { - "proxy": { - "type": "integer", - "description": "Proxy Hosts Count" - }, - "redirection": { - "type": "integer", - "description": "Redirection Hosts Count" - }, - "stream": { - "type": "integer", - "description": "Streams Count" - }, - "dead": { - "type": "integer", - "description": "404 Hosts Count" - } - } - } - } - } -} diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs deleted file mode 100644 index a4394115..00000000 --- a/backend/eslint.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import globals from 'globals'; -import pluginJs from '@eslint/js'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; - -export default [{ files: ['**/*.js'], languageOptions: { sourceType: 'commonjs' } }, { languageOptions: { globals: globals.node } }, pluginJs.configs.recommended, eslintPluginPrettierRecommended]; diff --git a/backend/index.js b/backend/index.js deleted file mode 100755 index 13244e52..00000000 --- a/backend/index.js +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node - -const logger = require('./logger').global; - -async function appStart() { - const migrate = require('./migrate'); - const setup = require('./setup'); - const app = require('./app'); - const apiValidator = require('./lib/validator/api'); - const internalNginx = require('./internal/nginx'); - const internalCertificate = require('./internal/certificate'); - const internalIpRanges = require('./internal/ip_ranges'); - - return migrate - .latest() - .then(setup) - .then(() => { - return apiValidator.loadSchemas; - }) - .then(internalIpRanges.fetch) - .then(() => { - internalNginx.reload(); - internalCertificate.initTimer(); - internalIpRanges.initTimer(); - - const server = app.listen(48693, '127.0.0.1', () => { - logger.info('Backend PID ' + process.pid + ' listening on port 48693 ...'); - - 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); - }); -} - -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 f2fe9fdb..00000000 --- a/backend/internal/access-list.js +++ /dev/null @@ -1,511 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const batchflow = require('batchflow'); -const logger = require('../logger').access; -const error = require('../lib/error'); -const utils = require('../lib/utils'); -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'); - -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() - .insertAndFetch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - owner_user_id: access.token.getUserId(1), - }) - .then(utils.omitRow(omissions())); - }) - .then((row) => { - data.id = row.id; - - const 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) { - const promises = []; - const 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); - } - }); - - const 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) { - const 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, - }), - ); - } - }); - - const query = accessListClientModel.query().delete().where('access_list_id', data.id); - - return query.then(() => { - // Add new items - if (promises.length) { - return Promise.all(promises); - } - }); - } - }) - .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.[certificate,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(internalNginx.reload) - .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) => { - const 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).allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - if (!skip_masking && typeof row.items !== 'undefined' && row.items) { - row = internalAccessList.maskItems(row); - } - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (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 - const htpasswd_file = internalAccessList.getFilename(row); - - try { - fs.unlinkSync(htpasswd_file); - } catch { - // 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) => { - const 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').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }) - .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) => { - const 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/etc/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) => { - const htpasswd_file = internalAccessList.getFilename(list); - - // 1. remove any existing access file - try { - fs.unlinkSync(htpasswd_file); - } catch { - // 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 - .execFile('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/audit-log.js b/backend/internal/audit-log.js deleted file mode 100644 index cd6c6107..00000000 --- a/backend/internal/audit-log.js +++ /dev/null @@ -1,71 +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(() => { - const query = auditLogModel.query().orderBy('created_on', 'DESC').orderBy('id', 'DESC').limit(100).allowGraph('[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.withGraphFetched('[' + 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/certificate.js b/backend/internal/certificate.js deleted file mode 100644 index 97829cd4..00000000 --- a/backend/internal/certificate.js +++ /dev/null @@ -1,1125 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const https = require('https'); -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('../certbot-dns-plugins.json'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const certbot = require('../lib/certbot'); -const archiver = require('archiver'); -const crypto = require('crypto'); -const path = require('path'); -const { isArray } = require('lodash'); - -const certbotConfig = '/data/tls/certbot/config.ini'; -const certbotCommand = 'certbot --logs-dir /tmp/certbot-log --work-dir /tmp/certbot-work --config-dir /data/tls/certbot'; - -function omissions() { - return ['is_deleted']; -} - -const internalCertificate = { - allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], - intervalTimeout: 1000 * 60 * 60 * Number(process.env.CRT), - interval: null, - intervalProcessing: false, - - initTimer: () => { - logger.info('Certbot Renewal Timer initialized'); - internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.intervalTimeout); - }, - - /** - * Triggered by a timer, this will check for expiring hosts and renew their tls certs if required - */ - processExpiringHosts: () => { - if (!internalCertificate.intervalProcessing) { - internalCertificate.intervalProcessing = true; - logger.info('Renewing TLS certs close to expiry...'); - - const cmd = certbotCommand + ' renew --quiet ' + '--config "' + certbotConfig + '" ' + '--preferred-challenges "dns,http" ' + '--no-random-sleep-on-renew'; - - 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 certbot 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) { - const promises = []; - - certificates.map(function (certificate) { - promises.push( - internalCertificate - .getCertificateInfoFromFile('/data/tls/certbot/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().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - // Request a new Cert using Certbot. Let the fun begin. - if (certificate.meta.dns_challenge) { - return internalCertificate - .requestLetsEncryptSslWithDnsChallenge(certificate) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, throw err back - throw err; - }); - } else { - return internalCertificate - .requestLetsEncryptSsl(certificate) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, throw err back - throw err; - }); - } - } else { - return certificate; - } - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - // At this point, the certbot cert should exist on disk. - // Lets get the expiry date from the file and update the row silently - return internalCertificate - .getCertificateInfoFromFile('/data/tls/certbot/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() - .patchAndFetchById(row.id, data) - .then(utils.omitRow(omissions())) - .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 saved_row; - }); - }); - }); - }, - - /** - * @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) => { - const query = certificateModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[owner]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @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 = '/data/tls/certbot/live/npm-' + data.id; - - if (!fs.existsSync(zipDirectory)) { - throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists'); - } - - const certFiles = fs - .readdirSync(zipDirectory) - .filter((fn) => fn.endsWith('.pem')) - .map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); - const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`; - const opName = '/tmp/' + downloadName; - 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 Certbot 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) => { - const 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) => { - const query = certificateModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - const 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/tls/custom/npm-' + certificate.id; - - return new Promise((resolve, reject) => { - if (certificate.provider === 'letsencrypt') { - reject(new Error('Refusing to write certbot 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(); - } - }); - - fs.writeFile(dir + '/chain.pem', certificate.meta.intermediate_certificate, 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 - const 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 - const promises = []; - _.map(files, (content, type) => { - promises.push( - new Promise((resolve) => { - if (type === 'certificate_key') { - resolve(internalCertificate.checkPrivateKey(content)); - } else { - // this should handle `certificate` and intermediate certificate - resolve(internalCertificate.getCertificateInfo(content, true)); - } - }).then((res) => { - return { [type]: res }; - }), - ); - }); - - return Promise.all(promises).then((files) => { - let data = {}; - - _.each(files, (file) => { - data = _.assign({}, data, file); - }); - - return data; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Object} data.files - * @returns {Promise} - */ - upload: (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) => { - 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) => { - const randomName = crypto.randomBytes(8).toString('hex'); - const filepath = path.join('/tmp', 'certificate_' + randomName); - fs.writeFileSync(filepath, private_key); - 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) => { - const randomName = crypto.randomBytes(8).toString('hex'); - const filepath = path.join('/tmp', 'certificate_' + randomName); - fs.writeFileSync(filepath, certificate); - 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) => { - const 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) => { - 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 tls 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 Certbot certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - let cmd = certbotCommand + ' certonly ' + '--config "' + certbotConfig + '" ' + '--cert-name "npm-' + certificate.id + '" ' + '--authenticator webroot ' + '--preferred-challenges "dns,http" ' + '--domains "' + certificate.domain_names.join(',') + '"'; - - if (certificate.meta.letsencrypt_email === '') { - cmd = cmd + ' --register-unsafely-without-email '; - } else { - cmd = cmd + ' --email "' + certificate.meta.letsencrypt_email + '" '; - } - - 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.json`) - * @param {String | null} credentials the content of this providers credentials file - * @param {String} propagation_seconds the time to wait until the dns record should be changed - * @returns {Promise} - */ - requestLetsEncryptSslWithDnsChallenge: async (certificate) => { - await certbot.installPlugin(certificate.meta.dns_provider); - const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; - logger.info(`Requesting Certbot certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - const credentialsLocation = '/data/tls/certbot/credentials/credentials-' + certificate.id; - // Escape single quotes and backslashes - const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll("'", "\\'").replaceAll('\\', '\\\\'); - const credentialsCmd = `echo '${escapedCredentials}' | tee '${credentialsLocation}'`; - - let mainCmd = certbotCommand + ' certonly ' + '--config "' + certbotConfig + '" ' + '--cert-name "npm-' + certificate.id + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' + '--authenticator ' + dnsPlugin.full_plugin_name + ' ' + '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' + (certificate.meta.propagation_seconds !== undefined ? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds : ''); - - if (certificate.meta.letsencrypt_email === '') { - mainCmd = mainCmd + ' --register-unsafely-without-email '; - } else { - mainCmd = mainCmd + ' --email "' + certificate.meta.letsencrypt_email + '" '; - } - - logger.info('Command:', `${credentialsCmd} && ${mainCmd}`); - - try { - await utils.exec(credentialsCmd); - const result = await utils.exec(mainCmd); - logger.info(result); - return result; - } catch (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('/data/tls/certbot/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 Certbot certificates can be renewed'); - } - }); - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - renewLetsEncryptSsl: (certificate) => { - logger.info('Renewing Certbot certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const cmd = certbotCommand + ' renew --force-renewal ' + '--config "' + certbotConfig + '" ' + '--cert-name "npm-' + certificate.id + '" ' + '--preferred-challenges "dns,http" ' + '--no-random-sleep-on-renew'; - - 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 dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; - - if (!dnsPlugin) { - throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); - } - - logger.info(`Renewing Certbot certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - const mainCmd = certbotCommand + ' renew --force-renewal ' + '--config "' + certbotConfig + '" ' + '--cert-name "npm-' + certificate.id + '" ' + '--preferred-challenges "dns,http" ' + '--no-random-sleep-on-renew'; - - 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 Certbot certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const mainCmd = certbotCommand + ' revoke ' + '--config "' + certbotConfig + '" ' + '--cert-path "/data/tls/certbot/live/npm-' + certificate.id + '/privkey.pem" ' + '--cert-path "/data/tls/certbot/live/npm-' + certificate.id + '/fullchain.pem" ' + '--delete-after-revoke'; - - // Don't fail command if file does not exist - const delete_credentialsCmd = `rm -f '/data/tls/certbot/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 = '/data/tls/certbot/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) { - const 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) { - const 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 = '/tmp/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&locationid=10`; - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(formBody), - Connection: 'keep-alive', - 'User-Agent': 'NPMplus', - Accept: '*/*', - }, - }; - - 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 () { - try { - const parsedBody = JSON.parse(responseBody + ''); - if (res.statusCode !== 200) { - logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`); - resolve(undefined); - } else { - resolve(parsedBody); - } - } catch (err) { - if (res.statusCode !== 200) { - logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`); - } else { - logger.warn(`Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`); - } - resolve(undefined); - } - }); - }); - - // Make sure to write the request body. - req.write(formBody); - req.end(); - req.on('error', 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.error) { - logger.info(`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`); - return `other:${result.error.msg}`; - } 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/dead-host.js b/backend/internal/dead-host.js deleted file mode 100644 index 0eae1f82..00000000 --- a/backend/internal/dead-host.js +++ /dev/null @@ -1,450 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -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) => { - const 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 - const 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().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .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) => { - const 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 - const 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) => { - const query = deadHostModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[owner,certificate]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @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) => { - const query = deadHostModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - const query = 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/host.js b/backend/internal/host.js deleted file mode 100644 index 6ca5895d..00000000 --- a/backend/internal/host.js +++ /dev/null @@ -1,217 +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; - - const combined_data = _.assign({}, existing_data, data); - - if (!combined_data.certificate_id) { - combined_data.ssl_forced = false; - combined_data.hsts_subdomains = false; - } - - if (!combined_data.ssl_forced) { - combined_data.hsts_enabled = 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) { - const 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) => { - const 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) { - const 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, - 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) { - const 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 19b1f6bf..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 utils = require('../lib/utils'); -const internalNginx = require('./nginx'); - -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 * Number(process.env.IPRT), - interval: null, - interval_processing: false, - iteration_count: 0, - - initTimer: () => { - if (process.env.SKIP_IP_RANGES === 'false') { - 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 && process.env.SKIP_IP_RANGES === 'false') { - internalIpRanges.interval_processing = true; - logger.info('Fetching IP Ranges from online services...'); - - let ip_ranges = []; - - return internalIpRanges - .fetchUrl(CLOUDFRONT_URL) - .then((cloudfront_data) => { - const data = JSON.parse(cloudfront_data); - - if (data && typeof data.prefixes !== 'undefined') { - data.prefixes.map((item) => { - if (item.service === 'CLOUDFRONT') { - ip_ranges.push(item.ip_prefix); - } - }); - } - - 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) => { - const items = cloudfare_data.split('\n').filter((line) => regIpV4.test(line)); - ip_ranges = [...ip_ranges, ...items]; - }) - .then(() => { - return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); - }) - .then((cloudfare_data) => { - const items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line)); - ip_ranges = [...ip_ranges, ...items]; - }) - .then(() => { - const clean_ip_ranges = []; - ip_ranges.map((range) => { - if (range) { - clean_ip_ranges.push(range); - } - }); - - return 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) => { - const renderEngine = utils.getRenderEngine(); - return new Promise((resolve, reject) => { - let template = null; - const filename = '/data/nginx/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 }) - .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/nginx.js b/backend/internal/nginx.js deleted file mode 100644 index 5483f2d8..00000000 --- a/backend/internal/nginx.js +++ /dev/null @@ -1,376 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const config = require('../lib/config'); -const utils = require('../lib/utils'); -const error = require('../lib/error'); - -const NgxPidFilePath = '/usr/local/nginx/logs/nginx.pid'; - -const internalNginx = { - /** - * This will: - * - test the nginx config first to make sure it's OK - * - create / recreate the config for the host - * - test again - * - IF OK: update the meta with online status - * - IF BAD: update the meta with offline status and remove the config entirely - * - then reload nginx - * - * @param {Object|String} model - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - configure: (model, host_type, host) => { - let combined_meta = {}; - - return internalNginx - .test() - .then(() => { - // Nginx is OK - // We're deleting this config regardless. - // Don't throw errors, as the file may not exist at all - // Delete the .err file too - return internalNginx.deleteConfig(host_type, host, false, true); - }) - .then(() => { - return internalNginx.generateConfig(host_type, host); - }) - .then(() => { - // Test nginx again and update meta with result - return internalNginx - .test() - .then(() => { - // nginx is ok - combined_meta = _.assign({}, host.meta, { - nginx_online: true, - nginx_err: null, - }); - - return model.query().where('id', host.id).patch({ - meta: combined_meta, - }); - }) - .catch((err) => { - // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. - // It will always look like this: - // nginx: [alert] could not open error log file: open() "/dev/null" failed (6: No such device or address) - - const valid_lines = []; - const err_lines = err.message.split('\n'); - err_lines.map(function (line) { - if (line.indexOf('/dev/null') === -1) { - valid_lines.push(line); - } - }); - - if (config.debug()) { - 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(() => { - internalNginx.renameConfigAsError(host_type, host); - }) - .then(() => { - return internalNginx.deleteConfig(host_type, host, true); - }); - }); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - return combined_meta; - }); - }, - - /** - * @returns {Promise} - */ - test: () => { - if (config.debug()) { - logger.info('Testing Nginx configuration'); - } - - return utils.exec('nginx -tq'); - }, - - /** - * @returns {Promise} - */ - - reload: () => { - return internalNginx.test().then(() => { - if (fs.existsSync(NgxPidFilePath)) { - const ngxPID = fs.readFileSync(NgxPidFilePath, 'utf8').trim(); - if (ngxPID.length > 0) { - logger.info('Reloading Nginx'); - utils.exec('nginx -s reload'); - } else { - logger.info('Starting Nginx'); - utils.execfg('nginx -e stderr'); - } - } else { - logger.info('Starting Nginx'); - utils.execfg('nginx -e stderr'); - } - }); - }, - - /** - * @param {String} host_type - * @param {Integer} host_id - * @returns {String} - */ - getConfigName: (host_type, host_id) => { - if (host_type === 'default') { - return '/data/nginx/default.conf'; - } - return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf'; - }, - - /** - * Generates custom locations - * @param {Object} host - * @returns {Promise} - */ - renderLocations: (host) => { - return new Promise((resolve, reject) => { - let template; - - try { - template = fs.readFileSync(__dirname + '/../templates/_location.conf', { encoding: 'utf8' }); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - const renderEngine = utils.getRenderEngine(); - let renderedLocations = ''; - - const locationRendering = async () => { - for (let i = 0; i < host.locations.length; i++) { - const locationCopy = Object.assign({}, { access_list_id: host.access_list_id }, { certificate_id: host.certificate_id }, { ssl_forced: host.ssl_forced }, { caching_enabled: host.caching_enabled }, { block_exploits: host.block_exploits }, { allow_websocket_upgrade: host.allow_websocket_upgrade }, { http2_support: host.http2_support }, { hsts_enabled: host.hsts_enabled }, { hsts_subdomains: host.hsts_subdomains }, { access_list: host.access_list }, { certificate: host.certificate }, host.locations[i]); - - if (locationCopy.forward_host.indexOf('/') > -1) { - const split = locationCopy.forward_host.split('/'); - - locationCopy.forward_host = split.shift(); - locationCopy.forward_path = `/${split.join('/')}`; - } - - renderedLocations += await renderEngine.parseAndRender(template, locationCopy); - } - }; - - locationRendering().then(() => resolve(renderedLocations)); - }); - }, - - /** - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - generateConfig: (host_type, host) => { - const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); - - if (config.debug()) { - logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2)); - } - - const renderEngine = utils.getRenderEngine(); - - return new Promise((resolve, reject) => { - let template = null; - const filename = internalNginx.getConfigName(nice_host_type, host.id); - - try { - template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', { encoding: 'utf8' }); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - let locationsPromise; - let origLocations; - - // Manipulate the data a bit before sending it to the template - if (nice_host_type !== 'default') { - host.use_default_location = true; - if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { - host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); - } - } - - 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 (config.debug()) { - logger.success('Wrote config:', filename, config_text); - } - - // Restore locations array - host.locations = origLocations; - - resolve(true); - }) - .catch((err) => { - if (config.debug()) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); - }); - }); - }); - }, - - /** - * A simple wrapper around unlinkSync that writes to the logger - * - * @param {String} filename - */ - deleteFile: (filename) => { - logger.debug('Deleting file: ' + filename); - try { - fs.unlinkSync(filename); - } catch (err) { - logger.debug('Could not delete file:', JSON.stringify(err, null, 2)); - } - }, - - /** - * - * @param {String} host_type - * @returns String - */ - getFileFriendlyHostType: (host_type) => { - return host_type.replace(new RegExp('-', 'g'), '_'); - }, - - /** - * @param {String} host_type - * @param {Object} [host] - * @param {Boolean} [delete_err_file] - * @returns {Promise} - */ - deleteConfig: (host_type, host, delete_err_file) => { - const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); - const config_file_err = config_file + '.err'; - - return new Promise((resolve /*, reject */) => { - internalNginx.deleteFile(config_file); - if (delete_err_file) { - internalNginx.deleteFile(config_file_err); - } - resolve(); - }); - }, - - /** - * @param {String} host_type - * @param {Object} [host] - * @returns {Promise} - */ - renameConfigAsError: (host_type, host) => { - const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); - const config_file_err = config_file + '.err'; - - return new Promise((resolve /*, reject */) => { - fs.unlink(config_file, () => { - // ignore result, continue - fs.rename(config_file, config_file_err, () => { - // also ignore result, as this is a debugging informative file anyway - resolve(); - }); - }); - }); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @returns {Promise} - */ - bulkGenerateConfigs: (host_type, hosts) => { - const promises = []; - hosts.map(function (host) { - promises.push(internalNginx.generateConfig(host_type, host)); - }); - - return Promise.all(promises); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @returns {Promise} - */ - bulkDeleteConfigs: (host_type, hosts) => { - const promises = []; - hosts.map(function (host) { - promises.push(internalNginx.deleteConfig(host_type, host, true)); - }); - - return Promise.all(promises); - }, - - /** - * @param {string} config - * @returns {boolean} - */ - advancedConfigHasDefaultLocation: function (cfg) { - return !!cfg.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/proxy-host.js b/backend/internal/proxy-host.js deleted file mode 100644 index f496b06c..00000000 --- a/backend/internal/proxy-host.js +++ /dev/null @@ -1,457 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -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) => { - const 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 - const 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().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .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) => { - const 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 - const 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(utils.omitRow(omissions())) - .then((saved_row) => { - // Add to audit log - return internalAuditLog - .add(access, { - action: 'updated', - object_type: 'proxy-host', - object_id: row.id, - meta: data, - }) - .then(() => { - return saved_row; - }); - }); - }) - .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) => { - const query = proxyHostModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[owner,access_list.[clients,items],certificate]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - row = internalHost.cleanRowCertificateMeta(row); - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @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) => { - const query = proxyHostModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - const query = 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 971fc0e5..00000000 --- a/backend/internal/redirection-host.js +++ /dev/null @@ -1,450 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -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) => { - const 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 - const 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().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .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; - }); - } - 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) => { - const 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 - const 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) => { - const query = redirectionHostModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[owner,certificate]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - row = internalHost.cleanRowCertificateMeta(row); - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @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) => { - const query = redirectionHostModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - const query = redirectionHostModel.query().count('id as count').where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first().then((row) => { - return parseInt(row.count, 10); - }); - }, -}; - -module.exports = internalRedirectionHost; diff --git a/backend/internal/report.js b/backend/internal/report.js deleted file mode 100644 index 9eda4cf5..00000000 --- a/backend/internal/report.js +++ /dev/null @@ -1,32 +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) => { - const user_id = access.token.getUserId(1); - - const 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 c3fc4a1a..00000000 --- a/backend/internal/setting.js +++ /dev/null @@ -1,125 +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/etc/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/stream.js b/backend/internal/stream.js deleted file mode 100644 index 5b041371..00000000 --- a/backend/internal/stream.js +++ /dev/null @@ -1,331 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -const streamModel = require('../models/stream'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); - -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().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .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() - .patchAndFetchById(row.id, data) - .then(utils.omitRow(omissions())) - .then((saved_row) => { - return internalNginx.configure(streamModel, 'stream', saved_row).then(() => { - return internalStream.get(access, { id: row.id, expand: ['owner'] }); - }); - }) - .then((saved_row) => { - // Add to audit log - return internalAuditLog - .add(access, { - action: 'updated', - object_type: 'stream', - object_id: row.id, - meta: data, - }) - .then(() => { - return saved_row; - }); - }); - }); - }, - - /** - * @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) => { - const query = streamModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[owner]').first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * @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) => { - const query = streamModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - const query = streamModel.query().count('id as count').where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first().then((row) => { - return parseInt(row.count, 10); - }); - }, -}; - -module.exports = internalStream; diff --git a/backend/internal/token.js b/backend/internal/token.js deleted file mode 100644 index 00394370..00000000 --- a/backend/internal/token.js +++ /dev/null @@ -1,155 +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) => { - const Token = new TokenModel(); - - data.scope = data.scope || 'user'; - data.expiry = data.expiry || '1d'; - - return userModel - .query() - .where('email', data.identity.toLowerCase().trim()) - .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 - const 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) => { - const Token = new TokenModel(); - - data = data || {}; - data.expiry = data.expiry || '1d'; - - if (access && access.token.getUserId(0)) { - // Create a moment of the expiry expression - const expiry = helpers.parseDatePeriod(data.expiry); - if (expiry === null) { - throw new error.AuthError('Invalid expiry time: ' + data.expiry); - } - - const 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, - 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, - }; - }); - }, -}; diff --git a/backend/internal/user.js b/backend/internal/user.js deleted file mode 100644 index 0992b22d..00000000 --- a/backend/internal/user.js +++ /dev/null @@ -1,482 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -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) => { - const auth = data.auth || null; - delete data.auth; - - data.avatar = data.avatar || ''; - data.roles = data.roles || []; - - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - - return access - .can('users:create', data) - .then(() => { - data.avatar = gravatar.url(data.email, { default: 'mm' }); - - return userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); - }) - .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 - const 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().patchAndFetchById(user.id, data).then(utils.omitRow(omissions())); - }) - .then(() => { - return internalUser.get(access, { id: data.id }); - }) - .then((user) => { - // Add to audit log - return internalAuditLog - .add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: data, - }) - .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(() => { - const query = userModel.query().where('is_deleted', 0).andWhere('id', data.id).allowGraph('[permissions]').first(); - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.withGraphFetched('[' + data.expand.join(', ') + ']'); - } - - return query.then(utils.omitRow(omissions())); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - row = _.omit(row, data.omit); - } - return row; - }); - }, - - /** - * Checks if an email address is available, but if a user_id is supplied, it will ignore checking - * against that user. - * - * @param email - * @param user_id - */ - isEmailAvailable: (email, user_id) => { - const query = userModel.query().where('email', '=', email.toLowerCase().trim()).where('is_deleted', 0).first(); - - if (typeof user_id !== 'undefined') { - query.where('id', '!=', user_id); - } - - return query.then((user) => { - return !user; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access - .can('users:delete', data.id) - .then(() => { - return internalUser.get(access, { id: data.id }); - }) - .then((user) => { - if (!user) { - throw new 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(() => { - const query = userModel.query().count('id as count').where('is_deleted', 0).first(); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('user.name', 'like', '%' + search_query + '%').orWhere('user.email', 'like', '%' + search_query + '%'); - }); - } - - return query; - }) - .then((row) => { - return 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(() => { - const query = userModel.query().where('is_deleted', 0).groupBy('id').allowGraph('[permissions]').orderBy('name', 'ASC'); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%').orWhere('email', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.withGraphFetched('[' + expand.join(', ') + ']'); - } - - return query.then(utils.omitRows(omissions())); - }); - }, - - /** - * @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, - }, - }); - }); - }) - .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/knexfile.js b/backend/knexfile.js deleted file mode 100644 index 81de3ed2..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 fe5715bf..00000000 --- a/backend/lib/access.js +++ /dev/null @@ -1,300 +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) { - const Token = new TokenModel(); - let token_data = null; - let initialized = false; - const 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 (initialized) { - 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) - .allowGraph('[permissions]') - .withGraphFetched('[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 { - initialized = true; - user_roles = user.roles; - permissions = user.permissions; - } - } else { - throw new error.AuthError('User cannot be loaded for Token'); - } - }); - } else { - initialized = 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 { - const 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) => { - const 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) => { - const base_object_type = permission_label.split(':').shift(); - - const 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(() => { - // initialized, token decoded ok - return this.getObjectSchema(permission).then((objectSchema) => { - const data_schema = { - [permission]: { - 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, - }, - }; - - const 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)); - - const 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/certbot.js b/backend/lib/certbot.js deleted file mode 100644 index 8ae3dd9c..00000000 --- a/backend/lib/certbot.js +++ /dev/null @@ -1,75 +0,0 @@ -const dnsPlugins = require('../certbot-dns-plugins.json'); -const utils = require('./utils'); -const error = require('./error'); -const logger = require('../logger').certbot; -const batchflow = require('batchflow'); - -const certbot = { - /** - * @param {array} pluginKeys - */ - installPlugins: async function (pluginKeys) { - let hasErrors = false; - - return new Promise((resolve, reject) => { - if (pluginKeys.length === 0) { - resolve(); - return; - } - - batchflow(pluginKeys) - .sequential() - .each((i, pluginKey, next) => { - certbot - .installPlugin(pluginKey) - .then(() => { - next(); - }) - .catch((err) => { - hasErrors = true; - next(err); - }); - }) - .error((err) => { - logger.error(err.message); - }) - .end(() => { - if (hasErrors) { - reject(new error.CommandError('Some plugins failed to install. Please check the logs above', 1)); - } else { - resolve(); - } - }); - }); - }, - - /** - * Installs a cerbot plugin given the key for the object from - * ../global/certbot-dns-plugins.json - * - * @param {string} pluginKey - * @returns {Object} - */ - installPlugin: async function (pluginKey) { - if (typeof dnsPlugins[pluginKey] === 'undefined') { - // throw Error(`Certbot plugin ${pluginKey} not found`); - throw new error.ItemNotFoundError(pluginKey); - } - - const plugin = dnsPlugins[pluginKey]; - logger.start(`Installing ${pluginKey}...`); - - const cmd = 'pip install --no-cache-dir ' + plugin.package_name; - return utils - .exec(cmd) - .then((result) => { - logger.complete(`Installed ${pluginKey}`); - return result; - }) - .catch((err) => { - throw err; - }); - }, -}; - -module.exports = certbot; diff --git a/backend/lib/config.js b/backend/lib/config.js deleted file mode 100644 index e7c1b9c1..00000000 --- a/backend/lib/config.js +++ /dev/null @@ -1,186 +0,0 @@ -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const logger = require('../logger').global; - -const keysFile = '/data/etc/npm/keys.json'; - -let instance = null; - -// 1. Load from config file first (not recommended anymore) -// 2. Use config env variables next -const configure = () => { - const filename = (process.env.NODE_CONFIG_DIR || '/data/etc/npm') + '/' + (process.env.NODE_ENV || 'default') + '.json'; - if (fs.existsSync(filename)) { - let configData; - try { - configData = require(filename); - } catch { - // do nothing - } - - if (configData && configData.database) { - logger.info(`Using configuration from file: ${filename}`); - instance = configData; - instance.keys = getKeys(); - return; - } - } - - const envMysqlHost = process.env.DB_MYSQL_HOST || null; - const envMysqlUser = process.env.DB_MYSQL_USER || null; - const envMysqlName = process.env.DB_MYSQL_NAME || null; - const envMysqlTls = process.env.DB_MYSQL_TLS || null; - const envMysqlCa = process.env.DB_MYSQL_CA || '/etc/ssl/certs/ca-certificates.crt'; - if (envMysqlHost && envMysqlUser && envMysqlName) { - // we have enough mysql creds to go with mysql - logger.info('Using MySQL configuration'); - instance = { - database: { - engine: 'mysql', - host: envMysqlHost, - port: process.env.DB_MYSQL_PORT || 3306, - user: envMysqlUser, - password: process.env.DB_MYSQL_PASSWORD, - name: envMysqlName, - ssl: envMysqlTls ? { ca: fs.readFileSync(envMysqlCa) } : false, - }, - keys: getKeys(), - }; - return; - } - - const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/etc/npm/database.sqlite'; - logger.info(`Using Sqlite: ${envSqliteFile}`); - instance = { - database: { - engine: 'knex-native', - knex: { - client: 'sqlite3', - connection: { - filename: envSqliteFile, - }, - useNullAsDefault: true, - }, - }, - keys: getKeys(), - }; -}; - -const getKeys = () => { - // Get keys from file - if (!fs.existsSync(keysFile)) { - generateKeys(); - } else if (process.env.DEBUG) { - logger.info('Keys file exists OK'); - } - try { - return require(keysFile); - } catch (err) { - logger.error('Could not read JWT key pair from config file: ' + keysFile, err); - process.exit(1); - } -}; - -const generateKeys = () => { - logger.info('Creating a new JWT key pair...'); - // Now create the keys and save them in the config. - const key = new NodeRSA({ b: 2048 }); - key.generateKeyPair(); - - const keys = { - key: key.exportKey('private').toString(), - pub: key.exportKey('public').toString(), - }; - - // Write keys config - try { - fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2)); - } catch (err) { - logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' + err.message); - process.exit(1); - } - logger.info('Wrote JWT key pair to config file: ' + keysFile); -}; - -module.exports = { - /** - * - * @param {string} key ie: 'database' or 'database.engine' - * @returns {boolean} - */ - has: function (key) { - instance === null && configure(); - const keys = key.split('.'); - let level = instance; - let has = true; - keys.forEach((keyItem) => { - if (typeof level[keyItem] === 'undefined') { - has = false; - } else { - level = level[keyItem]; - } - }); - - return has; - }, - - /** - * Gets a specific key from the top level - * - * @param {string} key - * @returns {*} - */ - get: function (key) { - instance === null && configure(); - if (key && typeof instance[key] !== 'undefined') { - return instance[key]; - } - return instance; - }, - - /** - * Is this a sqlite configuration? - * - * @returns {boolean} - */ - isSqlite: function () { - instance === null && configure(); - return instance.database.knex && instance.database.knex.client === 'sqlite3'; - }, - - /** - * Are we running in debug mode? - * - * @returns {boolean} - */ - debug: function () { - return !!process.env.DEBUG; - }, - - /** - * Returns a public key - * - * @returns {string} - */ - getPublicKey: function () { - instance === null && configure(); - return instance.keys.pub; - }, - - /** - * Returns a private key - * - * @returns {string} - */ - getPrivateKey: function () { - instance === null && configure(); - return instance.keys.key; - }, - - /** - * @returns {boolean} - */ - useLetsencryptStaging: function () { - return !!process.env.LE_STAGING; - }, -}; diff --git a/backend/lib/error.js b/backend/lib/error.js deleted file mode 100644 index b78c4630..00000000 --- a/backend/lib/error.js +++ /dev/null @@ -1,98 +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; - }, - - CommandError: function (stdErr, code, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = stdErr; - this.code = code; - this.public = false; - }, -}; - -_.forEach(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 8a529784..00000000 --- a/backend/lib/express/cors.js +++ /dev/null @@ -1,36 +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 38563d00..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; - const 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 adaabafa..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) { - const 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 ac01a66a..00000000 --- a/backend/lib/express/pagination.js +++ /dev/null @@ -1,53 +0,0 @@ -const _ = 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; - const myRegexp = /.*\.(asc|desc)$/gi; - const sort_array = []; - - sort = sort.split(','); - _.map(sort, function (val) { - const matches = myRegexp.exec(val); - - if (matches !== null) { - const dir = matches[1]; - sort_array.push({ - field: val.substr(0, val.length - (dir.length + 1)), - dir: dir.toLowerCase(), - }); - } else { - sort_array.push({ - field: val, - dir: 'asc', - }); - } - }); - - // Sort will now be in this format: - // [ - // { field: 'field1', dir: 'asc' }, - // { field: 'field2', dir: 'desc' } - // ] - - req.query.sort = sort_array; - next(); - }; -}; 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 bb09abba..00000000 --- a/backend/lib/helpers.js +++ /dev/null @@ -1,30 +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) { - const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m); - if (matches) { - return moment().add(matches[1], matches[2]); - } - - return null; - }, -}; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js deleted file mode 100644 index 21da446f..00000000 --- a/backend/lib/migrate_template.js +++ /dev/null @@ -1,54 +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 9b398325..00000000 --- a/backend/lib/utils.js +++ /dev/null @@ -1,138 +0,0 @@ -const _ = require('lodash'); -const exec = require('child_process').exec; -const spawn = require('child_process').spawn; -const execFile = require('child_process').execFile; -const { Liquid } = require('liquidjs'); -const error = require('./error'); -// const logger = require('../logger').global; - -module.exports = { - /** - * @param {String} cmd - */ - exec: async function (cmd, options = {}) { - // logger.debug('CMD:', cmd); - - const { stdout, stderr } = await new Promise((resolve, reject) => { - const child = exec(cmd, options, (isError, stdout, stderr) => { - if (isError) { - reject(new error.CommandError(stderr, isError)); - } else { - resolve({ stdout, stderr }); - } - }); - - child.on('error', (e) => { - reject(new error.CommandError(stderr, 1, e)); - }); - }); - return stdout; - }, - - /** - * @param {String} cmd - * @param {Array} args - */ - execFile: async function (cmd, args, options = {}) { - // logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : '')); - - const { stdout, stderr } = await new Promise((resolve, reject) => { - const child = execFile(cmd, args, options, (isError, stdout, stderr) => { - if (isError) { - reject(new error.CommandError(stderr, isError)); - } else { - resolve({ stdout, stderr }); - } - }); - - child.on('error', (e) => { - reject(new error.CommandError(stderr, 1, e)); - }); - }); - return stdout; - }, - - /** - * @param {String} cmd - */ - execfg: function (cmd) { - return new Promise((resolve, reject) => { - const childProcess = spawn(cmd, { - shell: true, - detached: true, - stdio: 'inherit', - }); - - childProcess.on('error', (err) => { - reject(err); - }); - - childProcess.on('close', (code) => { - if (code !== 0) { - reject(new Error(`Command '${cmd}' exited with code ${code}`)); - } else { - resolve(); - } - }); - }); - }, - - /** - * Used in objection query builder - * - * @param {Array} omissions - * @returns {Function} - */ - omitRow: function (omissions) { - /** - * @param {Object} row - * @returns {Object} - */ - return (row) => { - return _.omit(row, omissions); - }; - }, - - /** - * Used in objection query builder - * - * @param {Array} omissions - * @returns {Function} - */ - omitRows: function (omissions) { - /** - * @param {Array} rows - * @returns {Object} - */ - return (rows) => { - rows.forEach((row, idx) => { - rows[idx] = _.omit(row, omissions); - }); - return rows; - }; - }, - - /** - * @returns {Object} Liquid render engine - */ - getRenderEngine: function () { - const renderEngine = new Liquid({ - root: __dirname + '/../templates/', - }); - - /** - * nginxAccessRule expects the object given to have 2 properties: - * - * directive string - * address string - */ - renderEngine.registerFilter('nginxAccessRule', (v) => { - if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) { - return `${v.directive} ${v.address};`; - } - return ''; - }); - - return renderEngine; - }, -}; diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js deleted file mode 100644 index 9b577cde..00000000 --- a/backend/lib/validator/api.js +++ /dev/null @@ -1,43 +0,0 @@ -const error = require('../error'); -const path = require('path'); -const parser = require('@apidevtools/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')); - } - - const validate = ajv.compile(schema); - const valid = validate(payload); - - if (valid && !validate.errors) { - resolve(payload); - } else { - const message = ajv.errorsText(validate.errors); - const 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 419a9cf4..00000000 --- a/backend/lib/validator/index.js +++ /dev/null @@ -1,43 +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, - 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 { - const validate = ajv.compile(schema); - - const valid = validate(payload); - if (valid && !validate.errors) { - resolve(_.cloneDeep(payload)); - } else { - const message = ajv.errorsText(validate.errors); - reject(new 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 64c451c8..00000000 --- a/backend/logger.js +++ /dev/null @@ -1,14 +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 ' }), - certbot: new Signale({ scope: 'Certbot ' }), - 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 773109ab..00000000 --- a/backend/migrate.js +++ /dev/null @@ -1,14 +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 6377c163..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 51c2d3ee..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); -}; diff --git a/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js deleted file mode 100644 index 5a7c0574..00000000 --- a/backend/migrations/20181019052346_forward_host.js +++ /dev/null @@ -1,35 +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); -}; diff --git a/backend/migrations/20181113041458_http2_support.js b/backend/migrations/20181113041458_http2_support.js deleted file mode 100644 index 0ec6f124..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 c0834545..00000000 --- a/backend/migrations/20181213013211_forward_scheme.js +++ /dev/null @@ -1,35 +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 6731ea9e..00000000 --- a/backend/migrations/20190104035154_disabled.js +++ /dev/null @@ -1,56 +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 a3f2c744..00000000 --- a/backend/migrations/20190215115310_customlocations.js +++ /dev/null @@ -1,36 +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 3f994e4c..00000000 --- a/backend/migrations/20190218060101_hsts.js +++ /dev/null @@ -1,52 +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 196f4c06..00000000 --- a/backend/migrations/20190227065017_settings.js +++ /dev/null @@ -1,39 +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 a045c26b..00000000 --- a/backend/migrations/20200410143839_access_list_client.js +++ /dev/null @@ -1,51 +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 ff26690d..00000000 --- a/backend/migrations/20200410143840_access_list_client_fix.js +++ /dev/null @@ -1,35 +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 8f222978..00000000 --- a/backend/migrations/20201014143841_pass_auth.js +++ /dev/null @@ -1,42 +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 ef9e7e96..00000000 --- a/backend/migrations/20210210154702_redirection_scheme.js +++ /dev/null @@ -1,42 +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 b16d7b34..00000000 --- a/backend/migrations/20210210154703_redirection_status_code.js +++ /dev/null @@ -1,42 +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 55f9c8da..00000000 --- a/backend/migrations/20210423103500_stream_domain.js +++ /dev/null @@ -1,42 +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 82e6c403..00000000 --- a/backend/migrations/20211108145214_regenerate_default_host.js +++ /dev/null @@ -1,51 +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); -}; diff --git a/backend/models/access_list.js b/backend/models/access_list.js deleted file mode 100644 index 3de0a96f..00000000 --- a/backend/models/access_list.js +++ /dev/null @@ -1,86 +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); - }, - }, - items: { - relation: Model.HasManyRelation, - modelClass: AccessListAuth, - join: { - from: 'access_list.id', - to: 'access_list_auth.access_list_id', - }, - }, - clients: { - relation: Model.HasManyRelation, - modelClass: AccessListClient, - join: { - from: 'access_list.id', - to: 'access_list_client.access_list_id', - }, - }, - proxy_hosts: { - relation: Model.HasManyRelation, - modelClass: ProxyHost, - join: { - from: 'access_list.id', - to: 'proxy_host.access_list_id', - }, - modify: function (qb) { - qb.where('proxy_host.is_deleted', 0); - }, - }, - }; - } -} - -module.exports = AccessList; diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js deleted file mode 100644 index 0c066f19..00000000 --- a/backend/models/access_list_auth.js +++ /dev/null @@ -1,54 +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); - }, - }, - }; - } -} - -module.exports = AccessListAuth; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js deleted file mode 100644 index 41ad6a99..00000000 --- a/backend/models/access_list_client.js +++ /dev/null @@ -1,54 +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); - }, - }, - }; - } -} - -module.exports = AccessListClient; diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js deleted file mode 100644 index ec482bd8..00000000 --- a/backend/models/audit-log.js +++ /dev/null @@ -1,52 +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', - }, - }, - }; - } -} - -module.exports = AuditLog; diff --git a/backend/models/auth.js b/backend/models/auth.js deleted file mode 100644 index 4c194f25..00000000 --- a/backend/models/auth.js +++ /dev/null @@ -1,82 +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 */ - const _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, - }, - }, - }; - } -} - -module.exports = Auth; diff --git a/backend/models/certificate.js b/backend/models/certificate.js deleted file mode 100644 index 6c112a18..00000000 --- a/backend/models/certificate.js +++ /dev/null @@ -1,65 +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 = {}; - } - } - - $beforeUpdate() { - this.modified_on = now(); - } - - 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); - }, - }, - }; - } -} - -module.exports = Certificate; diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js deleted file mode 100644 index 725ebfef..00000000 --- a/backend/models/dead_host.js +++ /dev/null @@ -1,72 +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 = {}; - } - } - - $beforeUpdate() { - this.modified_on = now(); - } - - 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); - }, - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'dead_host.certificate_id', - to: 'certificate.id', - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - }, - }, - }; - } -} - -module.exports = DeadHost; diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js deleted file mode 100644 index 99a0778b..00000000 --- a/backend/models/now_helper.js +++ /dev/null @@ -1,12 +0,0 @@ -const db = require('../db'); -const config = require('../lib/config'); -const Model = require('objection').Model; - -Model.knex(db); - -module.exports = function () { - if (config.isSqlite()) { - return Model.raw("datetime('now','localtime')"); - } - return Model.raw('NOW()'); -}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js deleted file mode 100644 index 5959f213..00000000 --- a/backend/models/proxy_host.js +++ /dev/null @@ -1,84 +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 = {}; - } - } - - $beforeUpdate() { - this.modified_on = now(); - } - - 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); - }, - }, - 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); - }, - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'proxy_host.certificate_id', - to: 'certificate.id', - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - }, - }, - }; - } -} - -module.exports = ProxyHost; diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js deleted file mode 100644 index 5c42acfb..00000000 --- a/backend/models/redirection_host.js +++ /dev/null @@ -1,74 +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(); - } - - 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); - }, - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'redirection_host.certificate_id', - to: 'certificate.id', - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - }, - }, - }; - } -} - -module.exports = RedirectionHost; diff --git a/backend/models/setting.js b/backend/models/setting.js deleted file mode 100644 index 4a10f157..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 b7899d9b..00000000 --- a/backend/models/stream.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 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); - }, - }, - }; - } -} - -module.exports = Stream; diff --git a/backend/models/token.js b/backend/models/token.js deleted file mode 100644 index 5ce20d7c..00000000 --- a/backend/models/token.js +++ /dev/null @@ -1,126 +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 config = require('../lib/config'); -const error = require('../lib/error'); -const logger = require('../logger').global; -const ALGO = 'RS256'; - -module.exports = function () { - let token_data = {}; - - const self = { - /** - * @param {Object} payload - * @returns {Promise} - */ - create: (payload) => { - if (!config.getPrivateKey()) { - logger.error('Private key is empty!'); - } - // sign with RSA SHA256 - const options = { - algorithm: ALGO, - expiresIn: payload.expiresIn || '1d', - }; - - payload.jti = crypto.randomBytes(12).toString('base64').substring(-8); - - return new Promise((resolve, reject) => { - jwt.sign(payload, config.getPrivateKey(), options, (err, token) => { - if (err) { - reject(err); - } else { - token_data = payload; - resolve({ - token, - payload, - }); - } - }); - }); - }, - - /** - * @param {String} token - * @returns {Promise} - */ - load: function (token) { - if (!config.getPublicKey()) { - logger.error('Public key is empty!'); - } - return new Promise((resolve, reject) => { - try { - if (!token || token === null || token === 'null') { - reject(new error.AuthError('Empty token')); - } else { - jwt.verify(token, config.getPublicKey(), { 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; - 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) => { - const 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 5c429999..00000000 --- a/backend/models/user.js +++ /dev/null @@ -1,52 +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', - }, - }, - }; - } -} - -module.exports = User; diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js deleted file mode 100644 index e41508d4..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 742b65f6..00000000 --- a/backend/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "npmplus", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "index.js", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.5.5", - "ajv": "6.12.6", - "archiver": "7.0.1", - "batchflow": "0.4.0", - "bcrypt": "5.1.1", - "body-parser": "1.20.2", - "compression": "1.7.4", - "express": "4.19.2", - "express-fileupload": "1.5.0", - "gravatar": "1.8.2", - "jsonwebtoken": "9.0.2", - "knex": "3.1.0", - "liquidjs": "10.11.0", - "lodash": "4.17.21", - "moment": "2.30.1", - "mysql": "2.18.1", - "node-rsa": "1.1.1", - "objection": "3.1.4", - "path": "0.12.7", - "signale": "1.4.0", - "sqlite3": "5.1.6" - }, - "author": "Jamie Curnow and ZoeyVid ", - "license": "MIT", - "devDependencies": { - "@eslint/js": "9.1.1", - "eslint": "9.1.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-prettier": "5.1.3", - "globals": "15.0.0", - "prettier": "3.2.5" - } -} diff --git a/backend/password-reset.js b/backend/password-reset.js deleted file mode 100755 index c12df7b7..00000000 --- a/backend/password-reset.js +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env node - -// based on: https://github.com/jlesage/docker-nginx-proxy-manager/blob/796734a3f9a87e0b1561b47fd418f82216359634/rootfs/opt/nginx-proxy-manager/bin/reset-password - -const fs = require('fs'); -const bcrypt = require('bcrypt'); -const sqlite3 = require('sqlite3'); - -function usage() { - console.log(`usage: node ${process.argv[1]} USER_EMAIL PASSWORD - -Reset password of a NPMplus user. - -Arguments: - USER_EMAIL Email address of the user to reset the password. - PASSWORD Optional new password of the user. If not set, password - is set to 'changeme'. -`); - process.exit(1); -} - -const args = process.argv.slice(2); - -const USER_EMAIL = args[0]; -if (!USER_EMAIL) { - console.error('ERROR: User email address must be set.'); - usage(); -} - -const PASSWORD = args[1]; -if (!PASSWORD) { - console.error('ERROR: Password must be set.'); - usage(); -} - -if (fs.existsSync(process.env.DB_SQLITE_FILE)) { - bcrypt.hash(PASSWORD, 13, (err, PASSWORD_HASH) => { - if (err) { - console.error(err); - process.exit(1); - } - - const db = new sqlite3.Database(process.env.DB_SQLITE_FILE); - db.run( - `UPDATE auth SET secret = ? WHERE EXISTS - (SELECT * FROM user WHERE user.id = auth.user_id AND user.email = ?)`, - [PASSWORD_HASH, USER_EMAIL], - function (err) { - if (err) { - console.error(err); - process.exit(1); - } - - console.log(`Password for user ${USER_EMAIL} has been reset.`); - process.exit(0); - }, - ); - }); -} diff --git a/backend/routes/api/audit-log.js b/backend/routes/api/audit-log.js deleted file mode 100644 index 02d8811e..00000000 --- a/backend/routes/api/audit-log.js +++ /dev/null @@ -1,54 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalAuditLog = require('../../internal/audit-log'); - -const 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 25707ba1..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'); - -const router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true, -}); - -/** - * Health Check - * GET /api - */ -router.get('/', (req, res /*, next */) => { - const 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 fb09e5db..00000000 --- a/backend/routes/api/nginx/access_lists.js +++ /dev/null @@ -1,150 +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'); - -const 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 dc89152d..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'); - -const 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 Certbot 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 Certbot 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 43f406dc..00000000 --- a/backend/routes/api/nginx/dead_hosts.js +++ /dev/null @@ -1,198 +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'); - -const 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 b3afa5df..00000000 --- a/backend/routes/api/nginx/proxy_hosts.js +++ /dev/null @@ -1,198 +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'); - -const 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 e069308c..00000000 --- a/backend/routes/api/nginx/redirection_hosts.js +++ /dev/null @@ -1,198 +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'); - -const 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 24670306..00000000 --- a/backend/routes/api/nginx/streams.js +++ /dev/null @@ -1,198 +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'); - -const 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 820ac117..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'); - -const 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 44633017..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'); - -const 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 2b5afc18..00000000 --- a/backend/routes/api/settings.js +++ /dev/null @@ -1,97 +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'); - -const 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 031a6b2e..00000000 --- a/backend/routes/api/tokens.js +++ /dev/null @@ -1,53 +0,0 @@ -const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalToken = require('../../internal/token'); -const apiValidator = require('../../lib/validator/api'); - -const 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 fd186df1..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'); - -const 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 7f5b4dd2..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": 99, - "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 aea3e43c..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 c52fec34..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 Streams", - "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 5adff902..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": 99 - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 99 - } - } - }, - "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 28bea1b2..00000000 --- a/backend/schema/index.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "root", - "title": "NPMplus REST API", - "description": "This is the NPMplus 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/setup.js b/backend/setup.js deleted file mode 100644 index 0569e3b9..00000000 --- a/backend/setup.js +++ /dev/null @@ -1,145 +0,0 @@ -const config = require('./lib/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 certbot = require('./lib/certbot'); - -/** - * 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: iArhP1j7p1P6TA92FA2FMbbUGYqwcYzxC4AVEe12Wbi94FY9gNN62aKyF1shrvG4NycjjX9KfmDQiwkLZH1ZDR9xMjiG2QmoHXi'); - - const 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: 'iArhP1j7p1P6TA92FA2FMbbUGYqwcYzxC4AVEe12Wbi94FY9gNN62aKyF1shrvG4NycjjX9KfmDQiwkLZH1ZDR9xMjiG2QmoHXi', - 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 (config.debug()) { - logger.info('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 (config.debug()) { - logger.info('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) { - const plugins = []; - const promises = []; - - certificates.map(function (certificate) { - if (certificate.meta && certificate.meta.dns_challenge === true) { - if (plugins.indexOf(certificate.meta.dns_provider) === -1) { - plugins.push(certificate.meta.dns_provider); - } - - // Make sure credentials file exists - const credentials_loc = '/data/tls/certbot/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 /data/tls/certbot/credentials 2> /dev/null; echo '" + escapedCredentials + "' > '" + credentials_loc + "' && chmod 600 '" + credentials_loc + "'; }"; - promises.push(utils.exec(credentials_cmd)); - } - }); - - return certbot.installPlugins(plugins).then(() => { - if (promises.length) { - return Promise.all(promises).then(() => { - logger.info('Added Certbot plugins ' + plugins.join(', ')); - }); - } - }); - } - }); -}; - -module.exports = function () { - return setupDefaultUser().then(setupDefaultSettings).then(setupCertbotPlugins); -}; diff --git a/backend/sqlite-vaccum.js b/backend/sqlite-vaccum.js deleted file mode 100755 index bbf7e42b..00000000 --- a/backend/sqlite-vaccum.js +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const sqlite3 = require('sqlite3'); - -if (fs.existsSync(process.env.DB_SQLITE_FILE)) { - const db = new sqlite3.Database(process.env.DB_SQLITE_FILE, sqlite3.OPEN_READWRITE, (err) => { - if (err) { - console.error(err.message); - } else { - db.run('VACUUM; PRAGMA auto_vacuum = 1;', [], (err) => { - if (err) { - console.error(err.message); - } - db.close((err) => { - if (err) { - console.error(err.message); - } - }); - }); - } - }); -} diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf deleted file mode 100644 index 1562e6ed..00000000 --- a/backend/templates/_access.conf +++ /dev/null @@ -1,25 +0,0 @@ -{% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/etc/access/{{ access_list_id }}; - - {% if access_list.pass_auth == 0 %} - proxy_set_header Authorization ""; - {% endif %} - - {% endif %} - - # Access Rules: {{ access_list.clients | size }} total - {% for client in access_list.clients %} - {{client | nginxAccessRule}} - {% endfor %} - deny all; - - # Access checks must... - {% if access_list.satisfy_any == 1 %} - satisfy any; - {% else %} - satisfy all; - {% endif %} -{% endif %} diff --git a/backend/templates/_brotli.conf b/backend/templates/_brotli.conf deleted file mode 100644 index 39ef1286..00000000 --- a/backend/templates/_brotli.conf +++ /dev/null @@ -1,4 +0,0 @@ -{% if http2_support == 1 or http2_support == true -%} - # Enable Brotli - include conf.d/include/brotli.conf; -{% endif %} diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf deleted file mode 100644 index fff752d5..00000000 --- a/backend/templates/_certificates.conf +++ /dev/null @@ -1,15 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if certificate.provider == "letsencrypt" %} - # Certbot TLS - include conf.d/include/tls-ciphers.conf; - ssl_certificate /data/tls/certbot/live/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /data/tls/certbot/live/npm-{{ certificate_id }}/privkey.pem; - ssl_trusted_certificate /data/tls/certbot/live/npm-{{ certificate_id }}/chain.pem; -{% else %} - # Custom SSL - include conf.d/include/tls-ciphers.conf; - ssl_certificate /data/tls/custom/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /data/tls/custom/npm-{{ certificate_id }}/privkey.pem; - ssl_trusted_certificate /data/tls/custom/npm-{{ certificate_id }}/chain.pem; -{% endif %} -{% endif %} diff --git a/backend/templates/_forced_tls.conf b/backend/templates/_forced_tls.conf deleted file mode 100644 index 3b0226a2..00000000 --- a/backend/templates/_forced_tls.conf +++ /dev/null @@ -1,6 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} - # Force TLS - include conf.d/include/force-tls.conf; -{% endif %} -{% endif %} 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 c0a743ed..00000000 --- a/backend/templates/_hsts.conf +++ /dev/null @@ -1,17 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} -{% if hsts_enabled == 1 or hsts_enabled == true %} - more_clear_headers "Expect-CT"; - include conf.d/include/hsts.conf; -{% endif %} -{% endif %} -{% endif %} - -{% unless certificate and certificate_id > 0 -%} -{% unless ssl_forced == 1 or ssl_forced == true %} -{% unless hsts_enabled == 1 or hsts_enabled == true %} - more_clear_headers "Expect-CT"; - more_clear_headers "Strict-Transport-Security"; -{% endunless %} -{% endunless %} -{% endunless %} diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf deleted file mode 100644 index 8ad3be22..00000000 --- a/backend/templates/_listen.conf +++ /dev/null @@ -1,20 +0,0 @@ - listen unix:/run/nginx-{{ id }}.sock; - - listen 80; - listen [::]:80; - -{% if certificate and certificate_id > 0 %} - listen 443 ssl; - listen [::]:443 ssl; - - listen 443 quic; - listen [::]:443 quic; - -{% if hsts_subdomains == 1 or hsts_subdomains == true %} - more_set_headers 'Alt-Svc: h3=":443"; ma=86400'; -{% else %} - more_clear_headers "Alt-Svc"; - http3 off; -{% endif %} -{% endif %} - server_name {{ domain_names | join: " " }}; diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf deleted file mode 100644 index 5ff7f054..00000000 --- a/backend/templates/_location.conf +++ /dev/null @@ -1,17 +0,0 @@ - location {{ path }} { - set $forward_path "{{ forward_path }}"; - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - {% endif %} - - include conf.d/include/proxy-location.conf; - proxy_set_header X-Forwarded-Host $host{{ path }}; - if ($forward_path = "") { - rewrite ^{{ path }}(/.*)$ $1 break; - } - proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; - - {{ advanced_config }} - } diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf deleted file mode 100644 index adbf0ec4..00000000 --- a/backend/templates/dead_host.conf +++ /dev/null @@ -1,26 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled == 1 or enabled == true %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_tls.conf" %} -{% include "_brotli.conf" %} - -{{ advanced_config }} - include conf.d/include/acme-challenge.conf; - include conf.d/include/block-exploits.conf; -{% if use_default_location == 1 or use_default_location == true %} - location / { - include conf.d/include/acme-challenge.conf; - root /html/404; - try_files $uri /index.html; - } -{% endif %} - - # Custom - include /data/nginx/custom/server_dead.conf; - -} -{% endif %} diff --git a/backend/templates/default.conf b/backend/templates/default.conf deleted file mode 100644 index 5dd753ff..00000000 --- a/backend/templates/default.conf +++ /dev/null @@ -1,61 +0,0 @@ -# ------------------------------------------------------------ -# Default Site -# ------------------------------------------------------------ -server { - listen 80 default_server; - listen [::]:80 default_server; - - listen 443 ssl default_server; - listen [::]:443 ssl default_server; - - listen 443 quic reuseport default_server; - listen [::]:443 quic reuseport default_server; - more_set_headers 'Alt-Svc: h3=":443"; ma=86400'; - - server_name _; - - include conf.d/include/brotli.conf; - include conf.d/include/force-tls.conf; - include conf.d/include/tls-ciphers.conf; - include conf.d/include/acme-challenge.conf; - include conf.d/include/block-exploits.conf; - - #ssl_certificate ; - #ssl_certificate_key ; - #ssl_trusted_certificate ; - -{%- if value == "404" %} - location / { - include conf.d/include/acme-challenge.conf; - root /html/404; - try_files $uri /index.html; - } -{%- endif %} - -{%- if value == "444" %} - return 444; -{%- endif %} - -{%- if value == "redirect" %} - location / { - include conf.d/include/acme-challenge.conf; - return 307 {{ meta.redirect }}; - } -{%- endif %} - -{%- if value == "congratulations" %} - location / { - include conf.d/include/acme-challenge.conf; - root /html/default; - try_files $uri /index.html; - } -{%- endif %} - -{%- if value == "html" %} - location / { - include conf.d/include/acme-challenge.conf; - root /data/etc/html; - try_files $uri /index.html; - } -{%- 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/proxy_host.conf b/backend/templates/proxy_host.conf deleted file mode 100644 index a87eedbd..00000000 --- a/backend/templates/proxy_host.conf +++ /dev/null @@ -1,58 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled == 1 or enabled == true %} -server { - set $forward_scheme {{ forward_scheme }}; - set $server "{{ forward_host }}"; - set $port {{ forward_port }}; - -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_tls.conf" %} -{% include "_brotli.conf" %} -{% include "_access.conf" %} - - {% if block_exploits == 1 or block_exploits == true %} - modsecurity on; - {% if caching_enabled == 1 or caching_enabled == true -%} - modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf; - {% else %} - modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity.conf; - {% endif %} - {% endif %} - - include conf.d/include/acme-challenge.conf; - include conf.d/include/block-exploits.conf; - - {% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - {{ access_list.passauth }} - {% endif %} - {% endif %} - -{{ advanced_config }} - -{% if use_default_location == 1 or use_default_location == true %} - location / { - include conf.d/include/acme-challenge.conf; - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - {% endif %} - - # Proxy! - include conf.d/include/proxy.conf; - - # custom locations - {{ locations }} - } -{% endif %} - -{{ locations }} - - # 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 ec022134..00000000 --- a/backend/templates/redirection_host.conf +++ /dev/null @@ -1,28 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled == 1 or enabled == true %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_tls.conf" %} -{% include "_brotli.conf" %} - -{{ advanced_config }} - include conf.d/include/acme-challenge.conf; - include conf.d/include/block-exploits.conf; -{% if use_default_location == 1 or use_default_location == true %} - location / { - include conf.d/include/acme-challenge.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 d7740ad1..00000000 --- a/backend/templates/stream.conf +++ /dev/null @@ -1,29 +0,0 @@ -# ------------------------------------------------------------ -# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} -# ------------------------------------------------------------ - -{% if enabled == 1 or enabled == true %} -{% if tcp_forwarding == 1 or tcp_forwarding == true -%} -server { - listen {{ incoming_port }}; - listen [::]:{{ incoming_port }}; - - 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; - listen [::]:{{ incoming_port }} udp; - proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; - - # Custom - include /data/nginx/custom/server_stream.conf; - include /data/nginx/custom/server_stream_udp.conf; -} -{% endif %} -{% endif %} diff --git a/darkmode.css b/darkmode.css deleted file mode 100644 index 4215a75e..00000000 --- a/darkmode.css +++ /dev/null @@ -1,247 +0,0 @@ -body { - color: rgb(181, 175, 166) !important; - background-color: rgb(28, 30, 31) !important; -} --webkit-scrollbar { - background-color: #202324 !important; - color: #aba499 !important; -} --webkit-scrollbar { - background-color: #202324 !important; - color: #aba499 !important; -} --webkit-scrollbar-thumb { - background-color: #454a4d !important; -} -.avatar { - background-color: rgb(48, 52, 54) !important; - color: rgb(161, 152, 140) !important; -} -pre { - color: rgb(195, 190, 182) !important; - background-color: rgb(27, 29, 30) !important; - text-shadow: rgb(24, 26, 27) 0px 1px !important; -} -.close { - color: rgb(232, 230, 227) !important; - text-shadow: rgb(24, 26, 27) 0px 1px 0px !important; -} -.form-fieldset { - background-color: rgb(27, 30, 31) !important; - border-color: rgb(53, 58, 60) !important; -} -.modal-content { - background-color: rgb(24, 26, 27) !important; - border-color: rgba(140, 130, 115, 0.2) !important; -} -.modal-header { - border-bottom-color: rgb(53, 58, 60) !important; -} -.modal-footer { - border-top-color: rgb(53, 58, 60) !important; -} -.alert-secondary { - color: rgb(185, 179, 170) !important; - background-color: rgb(37, 40, 41) !important; - border-color: rgb(57, 62, 64) !important; -} -.nav-tabs { - color: rgb(174, 167, 156) !important; - border-bottom-color: rgb(56, 61, 63) !important; -} -.nav-tabs .nav-link.active, -.nav-tabs .nav-item.show .nav-link { - color: rgb(181, 175, 166) !important; - background-color: rgb(28, 30, 31) !important; - border-color: rgb(56, 61, 63) rgb(56, 61, 63) rgb(30, 46, 76) !important; -} -.nav-tabs .nav-link.active { - border-color: rgb(35, 77, 136) !important; - color: rgb(85, 151, 211) !important; - background-color: transparent !important; -} -.selectize-input.focus { - border-color: rgb(35, 77, 136) !important; - box-shadow: rgba(39, 86, 151, 0.25) 0px 0px 0px 2px !important; -} -.selectgroup-input:checked + .selectgroup-button { - border-color: rgb(35, 77, 136) !important; - color: rgb(85, 151, 211) !important; - background-color: rgb(30, 33, 34) !important; -} -.selectize-input, -.selectize-control.single .selectize-input.input-active { - background-color: rgb(24, 26, 27) !important; -} - -.selectize-dropdown, -.selectize-input, -.selectize-input input { - color: rgb(181, 175, 166) !important; -} -.selectize-input { - border-color: rgba(124, 115, 101, 0.12) !important; -} -.selectize-input, -.selectize-control.single .selectize-input.input-active { - background-color: rgb(24, 26, 27) !important; -} -.selectize-control.multi .selectize-input div { - background-color: rgb(35, 38, 39) !important; - color: rgb(181, 175, 166) !important; - border-color: rgba(124, 115, 101, 0.12) !important; -} -.selectize-dropdown, -.selectize-input, -.selectize-input input { - color: #495057 !important; - -webkit-font-smoothing: inherit !important; -} -.card { - background-color: rgb(24, 26, 27) !important; - border-color: rgba(124, 115, 101, 0.12) !important; -} -.tag { - color: rgb(155, 146, 133) !important; - background-color: rgb(35, 38, 39) !important; -} -.header { - background-color: rgb(24, 26, 27) !important; - border-bottom-color: rgba(124, 115, 101, 0.12) !important; -} -.navbar-light .navbar-brand { - color: rgba(232, 230, 227, 0.9) !important; -} -.nav-tabs { - color: rgb(174, 167, 156) !important; -} -.table th, -.text-wrap table th, -.table td, -.text-wrap table td { - border-top-color: rgb(56, 61, 63) !important; -} -.form-control { - color: rgb(181, 175, 166) !important; - background-color: rgb(24, 26, 27) !important; - border-color: rgba(124, 115, 101, 0.12) !important; -} -.footer { - background-color: rgb(24, 26, 27) !important; - border-top-color: rgba(124, 115, 101, 0.12) !important; - color: rgb(174, 167, 156) !important; -} -.text-default { - color: rgb(181, 175, 166) !important; -} -.text-yellow { - color: rgb(242, 202, 39) !important; -} -::selection { - background-color: #004daa !important; - color: #e8e6e3 !important; -} -.selection { - background-color: #004daa !important; - color: #e8e6e3 !important; -} -.dropdown-menu { - color: rgb(181, 175, 166) !important; - background-color: rgb(24, 26, 27) !important; - border-color: rgba(124, 115, 101, 0.12) !important; - box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px !important; -} -.dropdown-menu-arrow::before { - border-right-color: transparent !important; - border-left-color: transparent !important; - border-bottom-color: rgba(84, 91, 95, 0.2) !important; -} -.dropdown-menu-arrow::after { - border-right-color: transparent !important; - border-bottom-color: rgb(48, 52, 54) !important; - border-left-color: transparent !important; -} -.dropdown-divider { - border-top-color: rgb(53, 58, 60) !important; -} -.dropdown-menu-arrowafter { - border-right-color: transparent !important; - border-bottom-color: rgb(48, 52, 54) !important; - border-left-color: transparent !important; -} -.dropdown-item { - color: rgb(155, 146, 133) !important; -} -.btn-secondary { - color: rgb(181, 175, 166) !important; - background-color: rgb(24, 26, 27) !important; - border-color: rgba(124, 115, 101, 0.12) !important; - box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 1px 0px !important; - border-color: rgb(62 0 118 / 90%) !important; -} -.btn-teal { - color: rgb(232, 230, 227) !important; - background-color: rgb(34, 162, 149) !important; - border-color: rgb(32, 150, 137) !important; -} -.stamp { - color: rgb(232, 230, 227) !important; -} -.bg-yellow { - background-color: rgb(144, 117, 8) !important; -} -.bg-blue { - background-color: rgb(39, 86, 151) !important; -} -.bg-green { - background-color: rgb(75, 149, 0) !important; -} -.bg-red { - background-color: rgb(164, 26, 25) !important; -} -.custom-switch-indicator { - background-color: rgb(35, 38, 39) !important; - border-color: rgba(124, 115, 101, 0.12) !important; -} -.custom-switch-input:checked ~ .custom-switch-description { - color: rgb(181, 175, 166) !important; -} -.custom-switch-input:checked ~ .custom-switch-indicator { - background-color: rgb(34, 162, 149) !important; -} -.bg-success { - background-color: rgb(75, 149, 0) !important; -} -.btn-success { - color: rgb(232, 230, 227) !important; - background-color: rgb(75, 149, 0) !important; - border-color: rgb(101, 199, 0) !important; -} -.selectize-input.full { - background-color: rgb(24, 26, 27) !important; -} -.selectize-input, -.selectize-control.single .selectize-input.input-active { - background-color: rgb(24, 26, 27) !important; -} -.selectize-dropdown, -.selectize-input, -.selectize-input input { - color: rgb(181, 175, 166) !important; -} -.selectize-input { - border-color: rgba(124, 115, 101, 0.12) !important; -} -.selectize-dropdown { - color: rgb(202, 197, 190) !important; - background-color: rgb(24, 26, 27) !important; - border-right-color: rgb(61, 66, 69) !important; - border-bottom-color: rgb(61, 66, 69) !important; - border-left-color: rgb(61, 66, 69) !important; - box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px !important; -} -.input-group-text { - color: rgb(181, 175, 166) !important; - background-color: rgb(26, 28, 29) !important; - border-color: rgba(124, 115, 101, 0.12) !important; -} diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index c8f4b4f9..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -node_modules -webpack_stats.html -yarn-error.log diff --git a/frontend/app-images/default-avatar.jpg b/frontend/app-images/default-avatar.jpg deleted file mode 100644 index 798871673d0dc4721f00dbf5afb8e55b574b521e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1753 zcmb_dc~H|=5Z+&YAprt`C~}4S3@Az?2INpcOducz25jXhB2Yo1CLDrjYa&7wHG~93 zQ87Y6z@s7s0j(lnr5ws3mv~S_R1l3sMG>1u?Kt)y|7g4W-psz;otafJm%gZk!Bx2jP6UcuZAa4MY@_RfeVhR`%f|3w<6Oa`N4Ei4GCsV?qSUd)i z-vZ^2`ElN-+ULQ#$k&SFy^7YD)LlQ*d4R-JzQfc80}$*aNLV=!$W!+uO8WBv;T{2! z0}t?wHgJ_~2Uz%G4J1z3gWj|~EPizmG=8=22U+_JK-ee`8*BiG`8Lo>l>nwE3fyl3 zVEWb~5oLa^yvaLL3S3@)0PM!`tk-I^hlyW z`^^ww=eUCK5)V|hQZbLI0H$SY^ro#PfIKXS=M|ji0j3Br_niQgojHU*r2r1G;7n%$ zM2$8o7}EL&IUfl4Kn+Iob29#A{Dt}G|1_Vb2?&Z{@i-+^=_5)6gJMabWa=@?%>FC) z$eh{KfIu!^%f$Rjr{~%PoF*r+;_u5xmVz8~z<+3El~_CQJR@n^Vj#eB-}9p{ZD=er z$qu*WPBke>hv;SgS+1j!ts#r9lFD_~kJS#j@haKpItt0m#ekm*UE%Dos-Gn_F@77T zV`0swC>0yeACuv_=hp_TXC@8GN=nj?Y3w^Qn5*w**VEGDTOJTp z(`+fnvko^;f3ssQzrJH%lacm=3mz)DW7~U=)xKpvJ6vOD?Og<2WK zLw|Sg>Jg2*^_C_;m7u1TW__m?WaK8{yTgOBui?3L^<@*@y_~*54motpIbw&me)~94 zHN%UTg!Z;o>&N{Poy&8}?1?2X#Uj2@Q(&vdGJ$zZF){Oqnpe85)8d+(n-kBpc3u~^ zN58zr&+?3+FCkjIaM{|F?(8F7ZD>EV-UA!W?%o}`;nYSJzVu8FQ$>lv!AmOlbaSu1 zIZ$*{`n83iSC>7N@9g@Bi;v&htk#fd7&FEnznoq-dQf!5Iv|NztCn4rp-#)Ycw@J? zP#omQB6^Lc`idqADp`{ihF4RwH3BLxEih?O+Z_B-i&FH!=WT)3EE_>Go{Q_-FsY4F zgD_I0v1XEnD~j9_q5R!xhN*9RXahT_cvdk*y z5mALG!D#1MlZ?#JA*yjB!1eP?xpIzC_fwg)paf|F>VMOomNxZJSw z?ke{X3!0gO=B@98FN%l#d&B6KRgz)Hjja0gVfB(LX|wYQPYcE-&3PFr1Dmks7pC_M zjm7&9MwUJ)F6s6XR6TdSe&-cg*pkm(FRb;iV{o0en>WgVRWZ;K*OJnCdM5L?p-rCpeay{*WE5$G&tWk}0N}R@t&hB67$ELn%uC(cl%o*Z2bDkFYyU3wlP>qhf*BH|0 Z0{>nGgda5wQM_bG`|N&vAq%I0`Z* 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 G5}E)a52a54 diff --git a/frontend/app-images/favicons/android-chrome-512x512.png b/frontend/app-images/favicons/android-chrome-512x512.png deleted file mode 100644 index 6c69a48d882b60b0cdda80494a9e72131ada5919..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54834 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

VQwFh`Hjc6(Uf`bK=j6fYo` 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 diff --git a/frontend/app-images/favicons/apple-touch-icon.png b/frontend/app-images/favicons/apple-touch-icon.png deleted file mode 100644 index 111caf46fb1509f7fe219695462341f05bb7de01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15800 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஘>Bvz;i8Zhb1I~~U_0aoN? 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 - - - - - #333333 - - - diff --git a/frontend/app-images/favicons/favicon-16x16.png b/frontend/app-images/favicons/favicon-16x16.png deleted file mode 100644 index a456ed4f6e3f1a125e8d61709f9449c26eb3d559..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1220 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 Ef?^=LrvLx| diff --git a/frontend/app-images/favicons/favicon.ico b/frontend/app-images/favicons/favicon.ico deleted file mode 100644 index 9eae8b5f6c2c0feeb72b7aa99dd0fdedb840eab4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI32UwKXw)eq?ekbRc+%yZxpaL@tFcpSjK#Bzg>>3L;EU};`R*DrY2r9kzCWuC3 zk1@uAqQ*wGD+&mT)S00O48yzc{~Z_vj|P*Idv5OYeDgei_W<+G`mJ5p-fOF&p{dbR zgU8cAsiSc~PeVhbp`oFp)Bf|v8XC{=yN^-Qe_z&1Lt_^{z`|$nh=wyn|6cXdZ7cr& zzaLfu0fNIle7JEvKJ3$ftxtsta)$bIGkn+SdV4Qpe5do*qI*A02>;ea@V9`U1aPmA)x)-t1|V8>OZoEK$QQ;YI&gK=@anZ zhB8C>*_r;_%eQ@b@lQj=QDBdyAQB;oE{(Gf@l2W=p#7h23v;owKeSLRmgbK(m$vR4 zQS&ljWX(bFFYzBKH3_h-7y$NxKV6;pWU{O2=+#b-b zc6o%Fo^lG8L<-S&`tKbY+$-EXik&b%$Q#_HkRoYP(CN5wej|1;SNHz2{SZ!_b}ZXV z^Uv@@u&!>9z@>JJ&`JIr`~)g9`9{7(B9!*gWt?T}>70*)s!jB+5EtX8#En@(*6Tf2 zX)~*U1oE;enxCGJLijgH)qYXlMStQElYHRw!lN8u#A80*}ot8Lr&DZlj2746EzX{sIc5Mx0d)$O( zL4W&DKMcd4^+25u7p$p24)!Lf75v%YzeKY`S|+pWwiu5nsMBMdQ?aXkp!wap^4EdC zuC4yj2E5xO%9`ds(ZPRU@ktUiIzYC8wfqLy>)ZLC?q$3F3T_3|1RaGeg2kJXtwt|5D8MIEYx{e{7U;CMGf~*Jl*FsJF$rS``KM%J{*D)&@OslUktTEL^xAMIHL;TLpG()xI^?9cPL|>yZN(*Jzcz2zqG&FRxsz* zC*kJV%VWmo-HUb1ry!Cr`8pLl_5c+ji3M9M2@^*XKa;&1opmNL77hI-2>Q`^$W+@< z(=DRt5j)CbMtc`PgVo>pz7qTl=s8r)a=*$!KiUU&x-E!cZQTyxOt~Z2TU7i`a>Acd zO)>TE6Y#%)cIYzheEq)fjvbdj1nj=hs~CqI5@DH!wJb)(Px}xhNu0l6$dnaeC$tt0 zop#=T_yz-T7q{~ddf#`ef8W9HmRZemyF&9vw)0cqckjZFHUPhz#e9&Y|5d&K{N46X zu=j{{yd#RS&q|B1Os(t8A1O(o`4@n_rJes+!0=7X-^K5iN!Ncddwbq%J>b!z%P2hG1E(r*N*EYHx!J!Wpu0hN40(wf`Sl#*DezH(Zi13H5#%YN6{N3f{`u z$bb)vQ1`y@ftffVV0_$IUnzWe0(D#e2L8DWqtKEfPSR__O;yx!(-bM(6om+NKLS4d z2sI17t0Th2wIRZ3^7qEWh>iM>5!X0-SXR#sv8j1)Yyjr}f}+~_X+JU|CGivE9YY7E zP7mpoG&yKr(xgC=FwXBW{OGp-8y+du;p~51#7%9X`CA~ujccwHxYoT#vC$b@bE#shRZY`g>zb2$tZQr_>~8<{1?H9Q{B2`F68k#dA@pp@^ng#m zzYHQ!LBe?dZeu;eA4+sMX${5PeF_=aJy8Ewd2!7I{AzAh$zNUf?b{^g=2A9dqiFC; zAp(BlX;nkOPrzOch56akFyDR-bI{xT1obaZo*F0uw<%Qjq5P)z>lh|inxFHJC?3Oy z&%wWpo2(#w@7EOizlvSrUQb4OHFV(@%#rsOIm#}Ihf4{~PRmj(r50J%P&?aw@Ug5S zo`SM7ZhFtUtf!s^@hklVaUJsz-4-z;BPjSVCwXenfVlB~g+aF49{Y$q2EOh4+do@W zDH#?O%2M-U<*()i%6@1APgNhZ;&*_+5TCzj>MRx>*C&kGo&7P0df2qY|2_*-?#j8@#js(Cf0v zR5^t`LP!6h`NsZ2G`~tS z-3b5Z@r5Sg@~Cb({ll_&M+c>cUTtL-4b} z9s$+RKA23GlTPzdHO4^*q(^_Y^9$T7X?C?-2Bne6Nk?UeT~3)`e_)or=wv<0g0y)* zbUXiB7CfW*i@;8xMXY<|Q}#6~pOgNEzr7vaxqgAWH)IQ)1b-9wo6vtwK-SPZ*RSPo z@vj(^O2Wn+x&4#d>Hgq$f#`isLE8Ky-SXQMK4J*(*A%gHTL~+N?AOjAy?A-8L4q?_ zk301t_z(iUH+~SzmjB)K8~JQ=TiI-L6X|>7htb+6KASo4``B;4OzWusb&MZKko8YK z`Kbqcr7u2l7<*-sGX2owc*oRk!{B3AA?3tDN63OfhbT|Iv)FUqs0iU@ z3u8uIF-)9%@@1@D3f6+S$6?}_ZsWHRMfVv{5&R(5rDepTqIHW!xw045gE*8#NVfyl zvUVQ`H=(&!;8CA~7?s}VbQ?eG>tyuQ)5$P1(7BjCO8aCT*atQJWHWU0t;4cm}`Q7|FsQ=Rq`H zA1Lq>L%kc6?XXPBLHjuB|0r=Be1gm$k|E#C$(j*oa-BZ6O9>Tbk#LI(RD?wm-3E01 zci;aD?+&&vd3JZ&t4cDfQck8;){)WWO%1}Qt&6_8MtU3ECYuZ|QH{pmHxhPu8O8K_ zLYTf!h@s~TioZ^-_Co>wU(lxAR*$as zm{B3M0Q(W>wG(`pTvkVn^QkgcHW|mdL$nO;kWH);(mMS;k4R^6HdeHP`%Rp^s;*u* zM_v#9wl&SEmY7bIwIa^$SewOS8JWQ;lez1axO(W9^+9Ym#ZAAsBG~30iFL@M`CrG_ z=l>8hHb)XJj_P)NseJ7^nXe?J8OtwooX!8{pbOFaRIY4>OTuo zow=AyL2T5vN2mmQB&26%F6;Xpvt%XWq6+X6@E7`7KJZ8wpVRGm&xiP6TX;{Cliy%y znA;LtaKMQx%oCPq)E!(R39x^RmXx4cGx$M7dqDt z1HUh%fNIeO{@~_YaB6y@G=fsm2F>5HU(|p<9AdB2E%{TDci)5iAJ76Qk9~t`Ks`<_St(MD{y6|re)`+7{r7ByHO#t8mdYYqMz zP(H@Qc;2)!bIhT1{j1~a5;I%bR*9*!+d2Jx_Wz9E8(wPFEDN^Bp>VjL#xVYIXCFVn|Al>0yVIih-LXuK&Y zPeSOQmDX*gtbbdX(X*}e(o~fcqsf#0T+`OA47x1N=;=e3ziMcd(B(gH_Xwqu-mzM1 zXf!oxdTB89fG!7ED^+C^A%DluO$!L|!RMNKDy=WkfA1;luPS#G zX>}=k7HMUu{{E)y^7kF;=i0ufX&WKi=PG~n;)wQ?U$yP%CF+gOqHKaTbHQ(rnGfb^+0xk9G@eZnQf)I-qiJ|u2;II>Cd&5 z^gS&d$Oh`Tr`56Zz%T!gzrqD~`}uLsPgt+(Gw$23m-hWnxwDt`YmX4#O%e8x=>go! z4XcKF*5e*d+A_u(6X!uoKX@PTM|iiHSL*B;UcNg>{HP*Ckd1o;*U4s9EctHeMzX`BTg%o**~v|D7sSNh?1@7t|CSTl z!70q-qJ4xUMi?fF5k=dEyTNu2bS}=(&we|5_3vKO`MwQ35Zlvn@jk@#W#*Q$y?oA7 zBjn@fEEGOIwaxU^%gyYfW*_6bR4_k_3K9I;9ANZ4+$nww&{^Mlq4#D&_3%XbKt)Rx13m2h_Le6~1W4BMx1mqB4R^eS1cSERa! z;jUlUagQNXc$1{eI8AUarofrr$uP$-bL9R5a6U}D4c5L4VtZ^u&iprHO0H|kB*B{6 zJTMd?pBaIi>;#iJbz`~qB?bDrmvEQj9N`bSN-ftdr0uuj9z`zhf#jhdw1%4AhW%eC z*eClJ93y_ieM>j8TO5!JElcZ_C5S*(~ zq2fs5%isC)O1-(0kuTk_L4WDw2#MEC8C|u@CmW zAysT{X!1;oe01zkM!vDez$+c?nLPKtX59FEZmi?ogy_+iaF?eoXBA-{7mR#u7@c#5 zykqTlr6E_l?_KuwT~2GVHe5S^{PH#aw)*kdrzM~t%n>fAjmI8GiMBXnY*RXVNZ((3 ztDj%NEY)1C_hvsCKlw$Us1c{f!v0x|#a5IDSLiR8zF^4Ir0)!!9g%1KCC1J>Mg4r| zQrq84(#)47yXz-$+=6%6w{OcO()K6vE9m*484O+QuhdQ? z!J>|_FlU2-%O2!9-)Y~8+}9Kpw;bd>p<;~AC?{}^v?%C0pmS38@l9|1cao2U|8hEq zgLWV|vm?XJYREKhxzvUArv8z8idr^cA8{X=OoAKh^D0--g+(w#h z@grjSsv%oaMHxxTl&1x!RQ6T%z~Av&iu1Qv%uB{lhh1%rm>zmxo#!Eck;wo0tc=cC z*yHScCgVrq$Gv$XjGw~^;a|-UGfSq=%in2V^!Qgzob9JMlopdQ6|d>@eQ!}=tC(|E z`Gn5V8OPTWBkyvGzg|YqNyX4+Xb{%N!TgEko(7!TC9GlBh>)F6jUIN1jMx5=%+tC2 zXopd@X^1d;Z@A<(iL^R_bNi$Owa;&tlU}}TuRe#n|U$M~yr`?rpoBe}?VBv#VrVM-?`f!hRKU#m>z1S7tcNJq0zhe4Y{H161=e z>=(iQQ`o1lJ`u5vYEEdzcsMbcUv1*9`+O1Rg_>Pvk4s}6a<_uvH{%aAM*w-LY5u5@ zi?NUdhBDNFKkH1bk^Y{Cvkb#4kMZ`tEuH`jedwbQ!$m>W4v@{?|pxV?T?}5#>ZOjVjNaN zXL+GbBjEor!P@#W{P|TAgfpcTU}!X%EV~2y%V7WCj=$gL!FeD9XK4uc73I)1Zgk^( z_SD?jOuK6rjK<$D70j&h7S62w)At{pIXDLuJz(1uF~R{(Y<&f5>g_U|gQe+@&J^m6 zy!b!5|LV-a`+@Z3gvrMl%*k`m7oY##dE(CB^}qSP?Ysd0(#Hw((L#|HA&Jd|;(g@V zl+lAB_tNNJ)-$85RI5nq1*0kbxwUd>k)d%$JO&rO&mB#uWF=@R56I z1}ZapT2p*KPm(Z;kmq9vkq5TfYBx-uQ_vk%2kJPd_#K&spUsHoZsVWy*Xi#1m-`#| z%HMpsS;;;RloWm!z`L5~rstRIWxQ);tWC`4zxfdUh`V6@M3yqgS;&+4*;IrC3h(aq z$t~$J9lkIETH~Avo)lLV0FC+ccw#yV=8vDb+5+EGkAD^tHD!Q z5HCv_GxF@c{mzG@_gHL+^<;mS7--;OvgfNc9evu+_09<1+E<5!%j-Aur&R1`;r|iX z49Txqt5*~m#LtO}9eZ7x^v$u>J)&LtZu(17{J8Gvk(S%6)%J0RdzR7Gf+XC%4HeF- zy~4FGKc)9o{%|whr@i)$x!)^p+#Rb(>qDpU|5>gt)14dSBXNz08{<1o{X8eBdKNeN z^-auuH*j}7L%5{&Q`Ex{{9lvcjaD^*e#2!tVP=t^A@;r!ZW(zpVM4&3gbDsL)z6C` zw@gKR9UusAsvo=i^~FW8jeSif%EBbb`^`10CL4`k20Ql;@3)y5e+l>HWC0RypPj;W zlRBO+Q%)7!R{HYO6!NhN%@@CoZ|sXXFVxgtMr=jZWFh-y@Y4Sm(a&X2@+BW$qAbWd z(AV31OULv4dv#7mc`c#b6D<#MXYM91z2#G^TROOx6^NWsppB?jXZnBxpDq4!|7|zD zgZESC91Gny`)~)p&9ldIhZn!fG<%?o({}ddh)AoWS0b$voYegGB~NW9ys9p;DpG_S9dG)P9r%*sx>wa;9gW7^8O@bE zt+6zEK+PC>{gjX3USy=r)uKqNLmln6M6T@NR9R~(de9QVy56!|KcX<&ct<7f71aNN z_Pk&^QC?zhE{mCET)um=@w3Qi`@5!z)2cf9kD3Gd&VE?e9StwG(sQ{le`|vmf2lM~ zFjJa=eZ_8DlWGU#UHhp&|Gv`rnDTS({+1P-sD=cCX}Ph+6APA`PHX6P{B-_4lVYN2 z98|+Hn3vD|S|i}SYczefNqOQNm7$?ggxNN?r-nvmriR9~MH(7chP8=)Pya5XXWM7! K&(hD(&;B<*Tkw_u diff --git a/frontend/app-images/favicons/mstile-150x150.png b/frontend/app-images/favicons/mstile-150x150.png deleted file mode 100644 index 3586691f014b4fb1f42c6fb688309168b2224f43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11672 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 AZvX%Q diff --git a/frontend/app-images/favicons/safari-pinned-tab.svg b/frontend/app-images/favicons/safari-pinned-tab.svg deleted file mode 100644 index ab828900..00000000 --- a/frontend/app-images/favicons/safari-pinned-tab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/app-images/favicons/site.webmanifest b/frontend/app-images/favicons/site.webmanifest deleted file mode 100644 index 99d1016e..00000000 --- a/frontend/app-images/favicons/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/images/favicons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/images/favicons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/frontend/app-images/logo-256.png b/frontend/app-images/logo-256.png deleted file mode 100644 index 0ea6dd3d473df74d9cf1d03d2614f61e0de126de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29709 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 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 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 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 -<%- 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 2fb2b119..00000000 --- a/frontend/html/login.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% var title = 'Login – NPMplus' %> -<%- 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 9a4bce7d..00000000 --- a/frontend/html/partials/header.ejs +++ /dev/null @@ -1,73 +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/js/app/api.js b/frontend/js/app/api.js deleted file mode 100644 index c9d554f1..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 fdd82ae9..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 4c2a0c44..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 initially - 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 899fd970..00000000 --- a/frontend/js/app/nginx/certificates/form.ejs +++ /dev/null @@ -1,184 +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 f743f218..00000000 --- a/frontend/js/app/nginx/certificates/form.js +++ /dev/null @@ -1,297 +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('../../../../certbot-dns-plugins')); - -require('jquery-serializejson'); -require('selectize'); - -function sortProvidersAlphabetically(obj) { - return Object.entries(obj) - .sort((a,b) => a[1].name.toLowerCase() > b[1].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) { - alert('Intermediate Certificate file is not attached'); - return; - } else { - 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: 99, - 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 20d6f239..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].name %><% } %> - - - <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> - -<% if (canManage) { %> - - - -<% } %> 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 db273e90..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('../../../../../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 addc2b21..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 e0c4bb76..00000000 --- a/frontend/js/app/nginx/dead/form.js +++ /dev/null @@ -1,274 +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('../../../../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.hsts_subdomains); - 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.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: 99, - 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 74fec07d..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 45d78583..00000000 --- a/frontend/js/app/nginx/proxy/form.js +++ /dev/null @@ -1,357 +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('../../../../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.hsts_subdomains); - 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.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: 99, - 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 whether 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 6124c8a1..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 f3d689e2..00000000 --- a/frontend/js/app/nginx/redirection/form.ejs +++ /dev/null @@ -1,255 +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 ad677919..00000000 --- a/frontend/js/app/nginx/redirection/form.js +++ /dev/null @@ -1,276 +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('../../../../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.hsts_subdomains); - 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.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: 99, - 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 1fc4f134..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 d74ac0bd..00000000 --- a/frontend/js/app/settings/default-site/main.ejs +++ /dev/null @@ -1,57 +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 1623c4dc..00000000 --- a/frontend/js/app/settings/list/item.ejs +++ /dev/null @@ -1,21 +0,0 @@ - -
<%- i18n('settings', 'default-site') %>
-
- <%- i18n('settings', 'default-site-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 eaa57d99..00000000 --- a/frontend/js/app/ui/footer/main.ejs +++ /dev/null @@ -1,18 +0,0 @@ -
- -
- <%- i18n('main', 'version', {version: getVersion()}) %> - <%= i18n('footer', 'copy', {url: 'https://jc21.com'}) %> - <%= i18n('footer', 'copyzv', {url: 'https://zoeyvid.de'}) %> - <%= i18n('footer', 'theme', {url: 'https://tabler.github.io'}) %> -
-
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 874b6085..00000000 --- a/frontend/js/i18n/messages.json +++ /dev/null @@ -1,297 +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": "TLS", - "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": "NPMplus", - "version": "0.0.0", - "welcome": "Welcome to NPMplus", - "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": "User" - }, - "menu": { - "dashboard": "Dashboard", - "hosts": "Hosts" - }, - "footer": { - "fork-me": "Repository on GitHub", - "copy": "© 2024 jc21.com NPM", - "copyzv": "and © 2024 ZoeyVid NPMplus - MIT-License - ", - "theme": "Theme by Tabler v0.0.31" - }, - "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 HTTPS", - "force-ssl": "Force HTTPS", - "http2-support": "Enable Brotli", - "domain-names": "Domain Names", - "cert-provider": "Certificate Provider", - "block-exploits": "Enable ModSecurity", - "caching-enabled": "Enable CoreRuleSet (Requires ModSecurity)", - "ssl-certificate": "TLS Certificate", - "none": "None", - "new-cert": "Request a new TLS Certificate", - "with-le": "with Certbot", - "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, adding a location '/' will overwrite the proxy configuration", - "hsts-enabled": "Enable HSTS and security headers", - "hsts-subdomains": "Enable HTTP/3-Quic", - "locations": "Custom locations" - }, - "locations": { - "new_location": "Add location", - "path": "/path", - "location_label": "Define location", - "delete": "Delete" - }, - "ssl": { - "letsencrypt": "Certbot", - "other": "Custom", - "none": "HTTP only", - "letsencrypt-email": "Email Address for Certbot", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service / ToS of custom set CA", - "delete-ssl": "The TLS 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 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 TLS termination for your service that might not have TLS support built in.\nProxy Hosts are the most common use for the NPMplus.", - "access-list": "Access List", - "allow-websocket-upgrade": "Websockets Support", - "ignore-invalid-upstream-ssl": "Ignore Invalid TLS", - "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": "TLS Certificates", - "empty": "There are no TLS Certificates", - "add": "Add TLS Certificate", - "form-title": "Add {provider, select, letsencrypt{Certbot} other{Custom}} Certificate", - "delete": "Delete TLS Certificate", - "delete-confirm": "Are you sure you want to delete this TLS Certificate? Any hosts using it will need to be updated later.", - "help-title": "TLS Certificates", - "help-content": "TLS certificates (previously known as SSL Certificates) are a form of encryption key which allows your site to be encrypted for the end user.\nNPM uses by default a service called Let's Encrypt to issue TLS 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 NPMplus. 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 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": "Allow access if at least one authorization method succeeded", - "pass-auth": "Don't pass credentials to backend of 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-description": "What to show when Nginx is hit with an unknown Host", - "default-site-congratulations": "Congratulations Page", - "default-site-404": "404 Page", - "default-site-444": "Drop connection - only allows certbot dns-challenge", - "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 693bc050..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 deleted file mode 100644 index 60fa8bd9..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "npmplus", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "js/index.js", - "dependencies": { - "@babel/core": "7.24.4", - "babel-core": "6.26.3", - "babel-loader": "8.3.0", - "babel-preset-env": "1.7.0", - "backbone": "1.6.0", - "backbone.marionette": "4.1.3", - "copy-webpack-plugin": "5.1.2", - "css-loader": "5.2.7", - "ejs-lint": "2.0.0", - "ejs-loader": "0.4.1", - "ejs-webpack-loader": "2.2.2", - "file-loader": "6.2.0", - "html-webpack-plugin": "4.5.2", - "imports-loader": "0.8.0", - "jquery": "3.7.1", - "jquery-mask-plugin": "1.14.16", - "jquery-serializejson": "3.2.1", - "marionette.approuter": "1.0.2", - "marionette.templatecache": "1.0.0", - "messageformat": "2.3.0", - "messageformat-loader": "0.8.1", - "mini-css-extract-plugin": "1.6.2", - "moment": "2.30.1", - "node-sass": "7.0.3", - "nodemon": "3.1.0", - "numeral": "2.0.6", - "sass-loader": "10.5.2", - "style-loader": "4.0.0", - "tabler-ui": "git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813", - "underscore": "1.13.6", - "webpack": "4.47.0", - "webpack-cli": "4.10.0", - "webpack-visualizer-plugin": "0.1.11" - }, - "scripts": { - "build": "webpack --mode production" - }, - "author": "Jamie Curnow and ZoeyVid ", - "license": "MIT" -} 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 fe757ba3..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/webpack.config.js b/frontend/webpack.config.js deleted file mode 100644 index 6ce1636a..00000000 --- a/frontend/webpack.config.js +++ /dev/null @@ -1,143 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const Visualizer = require('webpack-visualizer-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const PACKAGE = require('./package.json'); - -module.exports = { - entry: { - main: './js/index.js', - login: './js/login.js' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: `js/[name].bundle.js?v=${PACKAGE.version}`, - chunkFilename: `js/[name].bundle.[id].js?v=${PACKAGE.version}`, - publicPath: '/' - }, - resolve: { - alias: { - 'tabler-core': 'tabler-ui/dist/assets/js/core', - 'bootstrap': 'tabler-ui/dist/assets/js/vendors/bootstrap.bundle.min', - 'sparkline': 'tabler-ui/dist/assets/js/vendors/jquery.sparkline.min', - 'selectize': 'tabler-ui/dist/assets/js/vendors/selectize.min', - 'tablesorter': 'tabler-ui/dist/assets/js/vendors/jquery.tablesorter.min', - 'vector-map': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-2.0.3.min', - 'vector-map-de': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-de-merc', - 'vector-map-world': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-world-mill', - 'circle-progress': 'tabler-ui/dist/assets/js/vendors/circle-progress.min', - 'c3': 'tabler-ui/dist/assets/js/vendors/chart.bundle.min' - } - }, - module: { - rules: [ - // Shims for tabler-ui - { - test: /assets\/js\/core/, - loader: 'imports-loader?bootstrap' - }, - { - test: /jquery-jvectormap-de-merc/, - loader: 'imports-loader?vector-map' - }, - { - test: /jquery-jvectormap-world-mill/, - loader: 'imports-loader?vector-map' - }, - - // other: - { - type: 'javascript/auto', // <= Set the module.type explicitly - test: /\bmessages\.json$/, - loader: 'messageformat-loader', - options: { - biDiSupport: false, - disablePluralKeyChecks: false, - formatters: null, - intlSupport: false, - locale: ['en'], - strictNumberSign: false - } - }, - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader' - } - }, - { - test: /\.ejs$/, - loader: 'ejs-loader' - }, - { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'sass-loader' - ] - }, - { - test: /.*tabler.*\.(jpe?g|gif|png|svg|eot|woff|ttf)$/, - use: [ - { - loader: 'file-loader', - options: { - outputPath: 'assets/tabler-ui/' - } - } - ] - }, - { - test: /source-sans-pro.*\.(woff(2)?)(\?v=\d+\.\d+\.\d+)?$/, - use: [ - { - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'assets/' - } - } - ] - } - ] - }, - plugins: [ - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - _: 'underscore' - }), - new HtmlWebpackPlugin({ - template: '!!ejs-webpack-loader!html/index.ejs', - filename: 'index.html', - inject: false, - templateParameters: { - version: PACKAGE.version - } - }), - new HtmlWebpackPlugin({ - template: '!!ejs-webpack-loader!html/login.ejs', - filename: 'login.html', - inject: false, - templateParameters: { - version: PACKAGE.version - } - }), - new MiniCssExtractPlugin({ - filename: 'css/[name].css', - chunkFilename: 'css/[id].css' - }), - new Visualizer({ - filename: '../webpack_stats.html' - }), - new CopyWebpackPlugin([{ - from: 'app-images', - to: 'images', - toType: 'dir' - }]) - ] -}; diff --git a/global/README.md b/global/README.md deleted file mode 100644 index 0c7cac50..00000000 --- a/global/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# certbot-dns-plugins - -This file contains info about available Certbot DNS plugins. -This only works for plugins which use the standard argument structure, so: ---authenticator ---credentials ---propagation-seconds - -File Structure: - -```json -{ - "cloudflare": { - "name": "Name displayed to the user", - "package_name": "Package name in PyPi repo", - "credentials": "Template of the credentials file", - "full_plugin_name": "The full plugin name as used in the commandline with certbot, e.g. 'dns-cloudflare'" - }, - ... -} -``` diff --git a/global/certbot-dns-plugins.json b/global/certbot-dns-plugins.json deleted file mode 100644 index 74b358de..00000000 --- a/global/certbot-dns-plugins.json +++ /dev/null @@ -1,338 +0,0 @@ -{ - "acmedns": { - "name": "ACME-DNS", - "package_name": "certbot-dns-acmedns", - "credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/tls/certbot/acme-registration.json", - "full_plugin_name": "dns-acmedns" - }, - "aliyun": { - "name": "Aliyun", - "package_name": "certbot-dns-aliyun", - "credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef", - "full_plugin_name": "dns-aliyun" - }, - "azure": { - "name": "Azure", - "package_name": "certbot-dns-azure", - "credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", - "full_plugin_name": "dns-azure" - }, - "bunny": { - "name": "bunny.net", - "package_name": "certbot-dns-bunny", - "credentials": "# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", - "full_plugin_name": "dns-bunny" - }, - "cloudflare": { - "name": "Cloudflare", - "package_name": "certbot-dns-cloudflare", - "credentials": "# Cloudflare API token\ndns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567\n# OR Cloudflare API credentials\n#dns_cloudflare_email = cloudflare@example.com\n#dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234", - "full_plugin_name": "dns-cloudflare" - }, - "cloudns": { - "name": "ClouDNS", - "package_name": "certbot-dns-cloudns", - "credentials": "# Target user ID (see https://www.cloudns.net/api-settings/)\n\tdns_cloudns_auth_id=1234\n\t# Alternatively, one of the following two options can be set:\n\t# dns_cloudns_sub_auth_id=1234\n\t# dns_cloudns_sub_auth_user=foobar\n\n\t# API password\n\tdns_cloudns_auth_password=password1", - "full_plugin_name": "dns-cloudns" - }, - "cloudxns": { - "name": "CloudXNS", - "package_name": "certbot-dns-cloudxns", - "credentials": "dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef\ndns_cloudxns_secret_key = 1122334455667788", - "full_plugin_name": "dns-cloudxns" - }, - "constellix": { - "name": "Constellix", - "package_name": "certbot-dns-constellix", - "credentials": "dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595\ndns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b\ndns_constellix_endpoint = https://api.dns.constellix.com/v1", - "full_plugin_name": "dns-constellix" - }, - "corenetworks": { - "name": "Core Networks", - "package_name": "certbot-dns-corenetworks", - "credentials": "dns_corenetworks_username = asaHB12r\ndns_corenetworks_password = secure_password", - "full_plugin_name": "dns-corenetworks" - }, - "cpanel": { - "name": "cPanel", - "package_name": "certbot-dns-cpanel", - "credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = user\ncpanel_password = hunter2", - "full_plugin_name": "cpanel" - }, - "desec": { - "name": "deSEC", - "package_name": "certbot-dns-desec", - "credentials": "dns_desec_token = YOUR_DESEC_API_TOKEN\ndns_desec_endpoint = https://desec.io/api/v1/", - "full_plugin_name": "dns-desec" - }, - "duckdns": { - "name": "DuckDNS", - "package_name": "certbot-dns-duckdns", - "credentials": "dns_duckdns_token=your-duckdns-token", - "full_plugin_name": "dns-duckdns" - }, - "digitalocean": { - "name": "DigitalOcean", - "package_name": "certbot-dns-digitalocean", - "credentials": "dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff", - "full_plugin_name": "dns-digitalocean" - }, - "directadmin": { - "name": "DirectAdmin", - "package_name": "certbot-dns-directadmin", - "credentials": "directadmin_url = https://my.directadminserver.com:2222\ndirectadmin_username = username\ndirectadmin_password = aSuperStrongPassword", - "full_plugin_name": "directadmin" - }, - "dnsimple": { - "name": "DNSimple", - "package_name": "certbot-dns-dnsimple", - "credentials": "dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", - "full_plugin_name": "dns-dnsimple" - }, - "dnsmadeeasy": { - "name": "DNS Made Easy", - "package_name": "certbot-dns-dnsmadeeasy", - "credentials": "dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\ndns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55", - "full_plugin_name": "dns-dnsmadeeasy" - }, - "dnspod": { - "name": "DNSPod", - "package_name": "certbot-dnspod", - "credentials": "certbot_dnspod_token = \ncertbot_dnspod_token_id = ", - "full_plugin_name": "certbot-dnspod" - }, - "domainoffensive": { - "name": "DomainOffensive (do.de)", - "package_name": "certbot-dns-do", - "credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN", - "full_plugin_name": "dns-do" - }, - "domeneshop": { - "name": "Domeneshop", - "package_name": "certbot-dns-domeneshop", - "credentials": "dns_domeneshop_client_token=YOUR_DOMENESHOP_CLIENT_TOKEN\ndns_domeneshop_client_secret=YOUR_DOMENESHOP_CLIENT_SECRET", - "full_plugin_name": "dns-domeneshop" - }, - "dynu": { - "name": "Dynu", - "package_name": "certbot-dns-dynu", - "credentials": "dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN", - "full_plugin_name": "dns-dynu" - }, - "easydns": { - "name": "easyDNS", - "package_name": "certbot-dns-easydns", - "credentials": "dns_easydns_usertoken = YOUR_EASYDNS_USERTOKEN\ndns_easydns_userkey = YOUR_EASYDNS_USERKEY\ndns_easydns_endpoint = https://rest.easydns.net", - "full_plugin_name": "dns-easydns" - }, - "eurodns": { - "name": "EuroDNS", - "package_name": "certbot-dns-eurodns", - "credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy", - "full_plugin_name": "dns-eurodns" - }, - "freedns": { - "name": "FreeDNS", - "package_name": "certbot-dns-freedns", - "credentials": "dns_freedns_username = myremoteuser\ndns_freedns_password = verysecureremoteuserpassword", - "full_plugin_name": "dns-freedns" - }, - "gandi": { - "name": "Gandi Live DNS", - "package_name": "certbot_plugin_gandi", - "credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN", - "full_plugin_name": "dns-gandi" - }, - "godaddy": { - "name": "GoDaddy", - "package_name": "certbot-dns-godaddy", - "credentials": "dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567\ndns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123", - "full_plugin_name": "dns-godaddy" - }, - "google": { - "name": "Google", - "package_name": "certbot-dns-google", - "credentials": "{\n\"type\": \"service_account\",\n...\n}", - "full_plugin_name": "dns-google" - }, - "googledomains": { - "name": "GoogleDomainsDNS", - "package_name": "certbot-dns-google-domains", - "credentials": "dns_google_domains_access_token = 0123456789abcdef0123456789abcdef01234567\ndns_google_domains_zone = \"example.com\"", - "full_plugin_name": "dns-google-domains" - }, - "he": { - "name": "Hurricane Electric", - "package_name": "certbot-dns-he", - "credentials": "dns_he_user = Me\ndns_he_pass = my HE password", - "full_plugin_name": "dns-he" - }, - "hetzner": { - "name": "Hetzner", - "package_name": "certbot-dns-hetzner", - "credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef", - "full_plugin_name": "dns-hetzner" - }, - "infomaniak": { - "name": "Infomaniak", - "package_name": "certbot-dns-infomaniak", - "credentials": "dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "full_plugin_name": "dns-infomaniak" - }, - "inwx": { - "name": "INWX", - "package_name": "certbot-dns-inwx", - "credentials": "dns_inwx_url = https://api.domrobot.com/xmlrpc/\ndns_inwx_username = your_username\ndns_inwx_password = your_password\ndns_inwx_shared_secret = your_shared_secret optional", - "full_plugin_name": "dns-inwx" - }, - "ionos": { - "name": "IONOS", - "package_name": "certbot-dns-ionos", - "credentials": "dns_ionos_prefix = myapikeyprefix\ndns_ionos_secret = verysecureapikeysecret\ndns_ionos_endpoint = https://api.hosting.ionos.com", - "full_plugin_name": "dns-ionos" - }, - "ispconfig": { - "name": "ISPConfig", - "package_name": "certbot-dns-ispconfig", - "credentials": "dns_ispconfig_username = myremoteuser\ndns_ispconfig_password = verysecureremoteuserpassword\ndns_ispconfig_endpoint = https://localhost:8080", - "full_plugin_name": "dns-ispconfig" - }, - "isset": { - "name": "Isset", - "package_name": "certbot-dns-isset", - "credentials": "dns_isset_endpoint=\"https://customer.isset.net/api\"\ndns_isset_token=\"\"", - "full_plugin_name": "dns-isset" - }, - "joker": { - "name": "Joker", - "package_name": "certbot-dns-joker", - "credentials": "dns_joker_username = \ndns_joker_password = \ndns_joker_domain = ", - "full_plugin_name": "dns-joker" - }, - "linode": { - "name": "Linode", - "package_name": "certbot-dns-linode", - "credentials": "dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\ndns_linode_version = [|3|4]", - "full_plugin_name": "dns-linode" - }, - "loopia": { - "name": "Loopia", - "package_name": "certbot-dns-loopia", - "credentials": "dns_loopia_user = user@loopiaapi\ndns_loopia_password = abcdef0123456789abcdef01234567abcdef0123", - "full_plugin_name": "dns-loopia" - }, - "luadns": { - "name": "LuaDNS", - "package_name": "certbot-dns-luadns", - "credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef", - "full_plugin_name": "dns-luadns" - }, - "namecheap": { - "name": "Namecheap", - "package_name": "certbot-dns-namecheap", - "credentials": "dns_namecheap_username = 123456\ndns_namecheap_api_key = 0123456789abcdef0123456789abcdef01234567", - "full_plugin_name": "dns-namecheap" - }, - "netcup": { - "name": "netcup", - "package_name": "certbot-dns-netcup", - "credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123", - "full_plugin_name": "dns-netcup" - }, - "njalla": { - "name": "Njalla", - "package_name": "certbot-dns-njalla", - "credentials": "dns_njalla_token = 0123456789abcdef0123456789abcdef01234567", - "full_plugin_name": "dns-njalla" - }, - "nsone": { - "name": "NS1", - "package_name": "certbot-dns-nsone", - "credentials": "dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw", - "full_plugin_name": "dns-nsone" - }, - "oci": { - "name": "Oracle Cloud Infrastructure DNS", - "package_name": "certbot-dns-oci", - "credentials": "[DEFAULT]\nuser = ocid1.user.oc1...\nfingerprint = xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx\ntenancy = ocid1.tenancy.oc1...\nregion = us-ashburn-1\nkey_file = ~/.oci/oci_api_key.pem", - "full_plugin_name": "dns-oci" - }, - "online": { - "name": "Online", - "package_name": "certbot-dns-online", - "credentials": "dns_online_token=0123456789abcdef0123456789abcdef01234567", - "full_plugin_name": "dns-online" - }, - "ovh": { - "name": "OVH", - "package_name": "certbot-dns-ovh", - "credentials": "dns_ovh_endpoint = ovh-eu\ndns_ovh_application_key = MDAwMDAwMDAwMDAw\ndns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\ndns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", - "full_plugin_name": "dns-ovh" - }, - "plesk": { - "name": "Plesk", - "package_name": "certbot-dns-plesk", - "credentials": "dns_plesk_username = your-username\ndns_plesk_password = secret\ndns_plesk_api_url = https://plesk-api-host:8443", - "full_plugin_name": "dns-plesk" - }, - "porkbun": { - "name": "Porkbun", - "package_name": "certbot-dns-porkbun", - "credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret", - "full_plugin_name": "dns-porkbun" - }, - "powerdns": { - "name": "PowerDNS", - "package_name": "certbot-dns-powerdns", - "credentials": "dns_powerdns_api_url = https://api.mypowerdns.example.org\ndns_powerdns_api_key = AbCbASsd!@34", - "full_plugin_name": "dns-powerdns" - }, - "regru": { - "name": "reg.ru", - "package_name": "certbot-regru", - "credentials": "dns_username=username\ndns_password=password", - "full_plugin_name": "dns" - }, - "rfc2136": { - "name": "RFC 2136", - "package_name": "certbot-dns-rfc2136", - "credentials": "# Target DNS server\ndns_rfc2136_server = 192.0.2.1\n# Target DNS port\ndns_rfc2136_port = 53\n# TSIG key name\ndns_rfc2136_name = keyname.\n# TSIG key secret\ndns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512", - "full_plugin_name": "dns-rfc2136" - }, - "strato": { - "name": "Strato", - "package_name": "certbot-dns-strato", - "credentials": "dns_strato_username = user\ndns_strato_password = pass\n# uncomment if you are using two factor authentication:\n# dns_strato_totp_devicename = 2fa_device\n# dns_strato_totp_secret = 2fa_secret\n#\n# uncomment if domain name contains special characters\n# insert domain display name as seen on your account page here\n# dns_strato_domain_display_name = my-punicode-url.de\n#\n# if you are not using strato.de or another special endpoint you can customise it below\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\n# dns_strato_custom_api_scheme = https\n# dns_strato_custom_api_host = www.strato.de\n# dns_strato_custom_api_port = 443\n# dns_strato_custom_api_path = \"/apps/CustomerService\"", - "full_plugin_name": "dns-strato" - }, - "timeweb": { - "name": "Timeweb Cloud", - "package_name": "certbot-dns-timeweb", - "credentials": "dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX", - "full_plugin_name": "dns-timeweb" - }, - "transip": { - "name": "TransIP", - "package_name": "certbot-dns-transip", - "credentials": "dns_transip_username = my_username\ndns_transip_key_file = /data/tls/certbot/transip-rsa.key", - "full_plugin_name": "dns-transip" - }, - "tencentcloud": { - "name": "Tencent Cloud", - "package_name": "certbot-dns-tencentcloud", - "credentials": "dns_tencentcloud_secret_id = TENCENT_CLOUD_SECRET_ID\ndns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY", - "full_plugin_name": "dns-tencentcloud" - }, - "vultr": { - "name": "Vultr", - "package_name": "certbot-dns-vultr", - "credentials": "dns_vultr_key = YOUR_VULTR_API_KEY", - "full_plugin_name": "dns-vultr" - }, - "websupport": { - "name": "Websupport.sk", - "package_name": "certbot-dns-websupport", - "credentials": "dns_websupport_identifier = \ndns_websupport_secret_key = ", - "full_plugin_name": "dns-websupport" - } -}