Compare commits

..
196 Commits
Author SHA1 Message Date
Alexander Holliday e052adfd91 Merge pull request #927 from bluewave-labs/fix/maintenance-access-rights
Allow demo user to create maintenance windows
2024-10-11 10:05:42 +08:00
Alex Holliday 34e2ecd194 Allow demo user to create maintenance windows 2024-10-11 10:04:20 +08:00
Alex Holliday 7a8d909d50 Merge remote-tracking branch 'upstream/develop' into develop 2024-10-11 09:39:29 +08:00
Alex Holliday 6f3cf2759a Merge branch 'master' into develop 2024-10-11 09:38:44 +08:00
Alexander Holliday 5d859e5d75 Merge pull request #922 from bluewave-labs/feat/server-auth-controller-tests
Feat/server auth controller tests
2024-10-11 06:33:15 +08:00
Alexander Holliday d20cc35f18 Merge pull request #921 from bluewave-labs/feat/be/error-handling
Refactor BE error handling
2024-10-10 15:26:54 -07:00
Alex Holliday 221a34906b add instanbul for coverage 2024-10-10 21:48:17 +08:00
Alex Holliday d757aabd57 Add test for getAllUsers 2024-10-10 21:30:21 +08:00
Alex Holliday 606c799166 Add deleteUser tests 2024-10-10 21:26:43 +08:00
Alex Holliday 387975ae19 Add resetPassword test 2024-10-10 19:34:08 +08:00
Alex Holliday 2a15c1589e Add validateRecovery test 2024-10-10 19:18:25 +08:00
Alex Holliday a430ccc051 Add tests for requestRecovery 2024-10-10 19:10:25 +08:00
Alex Holliday b24208f73e Add checkSuperadminExists test 2024-10-10 18:56:32 +08:00
Alex Holliday 5d9b1dd6ef Add authController.editUser tests, remove unused imports from 2024-10-10 12:16:30 +08:00
Alex Holliday a46f5ae3ab Add tests for authController.registerUser and authController.loginUser 2024-10-10 11:42:33 +08:00
Alex Holliday 485e520dcb Add mocha, chai, and sinon for testing 2024-10-10 11:42:04 +08:00
Alex Holliday 8578304ca4 refactor validation error and general error handling to reduce code duplicatoin 2024-10-10 08:59:44 +08:00
Dr. Gorkem Cetin 7833315fa4 GitBook: No commit message 2024-10-09 23:53:18 +00:00
Alex Holliday 9b2480d8b3 Merge branch 'master' of github.com:bluewave-labs/bluewave-uptime 2024-10-10 07:52:48 +08:00
Alex Holliday 00aa848dcf Create guides folder 2024-10-10 07:51:45 +08:00
Gorkem Cetin (BWL) bc001c6a0e Delete docs directory 2024-10-09 19:48:13 -04:00
Alexander Holliday 53445befea Update quickstart.md 2024-10-10 07:42:38 +08:00
Alexander Holliday 69ec9f95d8 Merge pull request #920 from bluewave-labs/feat/docs-update
docs/update -> master
2024-10-09 16:40:20 -07:00
Alex Holliday 7984972191 update docs 2024-10-10 07:39:25 +08:00
Alexander Holliday 389b5bdf56 Merge pull request #919 from bluewave-labs/feat/documentation-update
Update quickstart documentation
2024-10-09 16:33:43 -07:00
Alex Holliday fe22e778ff Update quickstart documentation 2024-10-10 07:32:31 +08:00
Alexander Holliday 0d12c78b1f Merge pull request #918 from bluewave-labs/feat/flow-docs
Feat/flow docs
2024-10-09 16:17:42 -07:00
Alex Holliday 2c0d02c84e move diagrams to /docs/diagrams 2024-10-10 07:16:45 +08:00
Alex Holliday 2f2d354576 Merge branch 'develop' into feat/flow-docs 2024-10-10 07:15:21 +08:00
Alexander Holliday 82de7ed47c Merge pull request #917 from bluewave-labs/master
Master -> Develop
2024-10-09 16:14:18 -07:00
Alexander Holliday 5001c5cb47 Merge pull request #861 from bluewave-labs/feat/maintenance-page
feat: Maintenance Page, resovles #674
2024-10-09 16:09:03 -07:00
Alex Holliday c9ba0be2c5 refactor logger for clarity 2024-10-09 14:45:02 +08:00
Alex Holliday a798f98ff4 Update monitors page Autocomplete field to use inputValue prop instead fo value, logger minor tweak 2024-10-09 14:42:27 +08:00
Alex Holliday 6e95d28e27 Implement pausing/resuming maintenance windows 2024-10-09 14:28:44 +08:00
Alex Holliday ad9d735127 implement editing maintenance windows 2024-10-09 14:10:38 +08:00
Gorkem Cetin (BWL) df304ced93 Delete documentation directory 2024-10-09 00:11:33 -04:00
Dr. Gorkem Cetin 86bed1b3b2 GITBOOK-20: No subject 2024-10-09 04:10:02 +00:00
Dr. Gorkem Cetin bde789dc59 GITBOOK-20: No subject 2024-10-09 04:06:51 +00:00
Alex Holliday e3c0944062 Add route, controller, and DB methods for fetching maintenance window by ID 2024-10-09 12:06:01 +08:00
Alex Holliday 114cfd6cb7 add put route, controller, and db method for maintenance window 2024-10-09 11:33:12 +08:00
Alex Holliday 7c5c821fdd add missed files, add logger 2024-10-09 11:10:29 +08:00
Alex Holliday 335098e2a8 implement deleting maintenance window 2024-10-09 11:07:14 +08:00
Alex Holliday f48b769baa Merge remote-tracking branch 'upstream/develop' into feat/maintenance-page 2024-10-08 09:49:26 +08:00
Alexander Holliday 0a89c595de Merge pull request #911 from bluewave-labs/feat/logout-user-on-jwt-expire
Feat/logout user on jwt expire
2024-10-06 23:44:11 -07:00
Alex Holliday 4a3636789e Add token expired msg 2024-10-07 13:54:00 +08:00
Alex Holliday ef4bfec3f2 Refactor network service, clear auth and monitor state on bad token 2024-10-07 13:47:20 +08:00
Alexander Holliday f0c363e9bf Merge pull request #910 from bluewave-labs/develop
Develop -> Master
2024-10-06 20:08:47 -07:00
Alexander Holliday 1255e2c13d Merge pull request #909 from bluewave-labs/fix/base-url-logic
Correct base URL logic
2024-10-06 20:07:45 -07:00
Alex Holliday f4a8c226b2 Correct base URL logic 2024-10-07 11:06:26 +08:00
Dr. Gorkem Cetin 308e884b36 GITBOOK-20: No subject 2024-10-04 16:22:25 +00:00
Dr. Gorkem Cetin bdeb33b8d3 GITBOOK-19: No subject 2024-10-04 16:14:18 +00:00
Dr. Gorkem Cetin a9a34f8725 GITBOOK-18: No subject 2024-10-04 13:35:14 +00:00
Dr. Gorkem Cetin f7a3172251 GITBOOK-16: No subject 2024-10-04 13:20:04 +00:00
Dr. Gorkem Cetin 9dc3fe3a27 GITBOOK-15: No subject 2024-10-04 13:16:34 +00:00
Alex Holliday 8f348507dc Add actions menu, add time to next window 2024-10-04 15:44:35 +08:00
Alex Holliday 7b65debed6 Populate table 2024-10-04 15:25:41 +08:00
Alex Holliday b0182cff6d Add maintenance table 2024-10-04 14:10:16 +08:00
Alex Holliday 68296eccc3 Add getMaintenanceWindowsByTeamId 2024-10-04 13:49:53 +08:00
Gorkem Cetin (BWL) 211a1a2711 Installation documents are now on Gitbook. 2024-10-04 01:24:17 -04:00
Alex Holliday 686e1f09d9 Add maintenance redirects 2024-10-04 13:19:04 +08:00
Dr. Gorkem Cetin dbda203ba3 GITBOOK-13: No subject 2024-10-04 05:18:17 +00:00
Gorkem Cetin (BWL) f3e7e5eca6 testing sync 2024-10-04 01:15:02 -04:00
Dr. Gorkem Cetin 35ae579509 GITBOOK-12: No subject 2024-10-04 05:13:57 +00:00
Alex Holliday 22a0caf919 Fix date/time error in JobQueue 2024-10-04 13:04:18 +08:00
Gorkem Cetin (BWL) acc673c244 Add user's guide 2024-10-04 00:56:37 -04:00
Alex Holliday f5547a1825 add network util method to submit form 2024-10-04 12:39:52 +08:00
Alex Holliday 8e87bdccd0 Add teamID to maintenance-window 2024-10-04 12:31:44 +08:00
Alex Holliday 47e051c2bb update backend for inserting maintenance windows 2024-10-04 12:24:40 +08:00
Alex Holliday c8418705b1 parse form for submission 2024-10-04 12:09:08 +08:00
Alex Holliday 5f96b17595 Add error validation 2024-10-04 11:40:01 +08:00
Alex Holliday 6ac19000f7 add controls to all inputs 2024-10-03 15:24:22 +08:00
Alex Holliday fa106d99ed Add autocomplete component to create maintenance window page 2024-10-03 09:12:59 +08:00
Alex Holliday 361991141a Merge remote-tracking branch 'upstream/develop' into feat/maintenance-page 2024-10-03 08:20:36 +08:00
Alexander Holliday d305c63a27 Merge pull request #906 from bluewave-labs/feat/docs/openapi-update
add distribution server option to openapi docs
2024-10-02 17:09:09 -07:00
Alexander Holliday c5522a710a Update README.md 2024-10-03 08:07:03 +08:00
Alex Holliday 79d105e80e add distribution server option to openapi docs 2024-10-02 15:27:47 +08:00
Alexander Holliday 4c545f3d5a Merge pull request #905 from bluewave-labs/feat/fe/fallback-api-url
Add a fallback URL
2024-10-02 00:10:15 -07:00
Alex Holliday 1cdb20e678 Add a fallback URL 2024-10-02 15:09:01 +08:00
Alex Holliday 133bf736d7 Merge remote-tracking branch 'upstream/develop' into feat/flow-docs 2024-10-02 14:59:47 +08:00
Alex Holliday fbe34d3488 Merge remote-tracking branch 'upstream/develop' into feat/flow-docs 2024-10-02 09:03:01 +08:00
Alex Holliday 8fcafc0694 Fix missed commit 2024-10-02 08:46:33 +08:00
Alexander Holliday d11fc90a1d Merge pull request #904 from bluewave-labs/develop
Develop -> Master
2024-10-01 17:41:17 -07:00
Alexander Holliday 1dbe405fc1 Merge pull request #903 from bluewave-labs/feat/docker-refactor
Feat/docker refactor
2024-10-01 17:40:15 -07:00
Alex Holliday 26df485644 Update client network serivce URL logic 2024-10-02 08:37:36 +08:00
Alex Holliday 2499925423 Refactor dockerfiles for clarity 2024-10-01 15:52:46 +08:00
Alexander Holliday 3a31387d00 Merge pull request #902 from bluewave-labs/feat/jobqueue-delete
Scale workers on deleting job
2024-09-30 22:58:46 -07:00
Alex Holliday 45a52d9e7c Add jobqueue flow chart 2024-10-01 13:57:17 +08:00
Alex Holliday 5b3b78bd3f Scale workers on deleting job 2024-10-01 12:36:37 +08:00
Gorkem Cetin (BWL) fc84d8e986 Fix cases 2024-09-30 23:13:30 -04:00
Gorkem Cetin (BWL) d507f5e7fe Fix cases 2024-09-30 23:09:35 -04:00
Alexander Holliday 3b6a610c96 Merge pull request #900 from bluewave-labs/feat/be/refactor
Feat/be/refactor
2024-09-30 20:03:09 -07:00
Alex Holliday 1f2a19fb43 Merge upstream/develop 2024-10-01 11:02:46 +08:00
Alex Holliday c1d1e0ad1d Refactor for clarity and correctness, add JS docs 2024-10-01 10:55:51 +08:00
Alex Holliday e0e80dc9c7 Refactor method names, add improve JSdocs for monitorController 2024-10-01 10:29:32 +08:00
Alex Holliday 6c598b976f Move db models to db folder 2024-10-01 10:01:25 +08:00
Alexander Holliday 960022d770 Merge pull request #899 from bluewave-labs/feat/be/documentation
Add JSdocs to SettingsService, add descriptions of services
2024-09-30 18:56:03 -07:00
Alex Holliday f724997b02 Add JSdocs to SettingsService, add descriptions of services 2024-10-01 09:55:14 +08:00
Gorkem Cetin (BWL) 53c468f0f3 Update README.md 2024-09-30 20:00:51 -04:00
Alexander Holliday c27ed0f700 Merge pull request #898 from bluewave-labs/develop
Develop -> Master
2024-09-30 00:21:46 -07:00
Alexander Holliday 34389d4878 Merge pull request #897 from bluewave-labs/fix/settings
removed console logs, removed permission check for getting settings
2024-09-30 00:21:17 -07:00
Alex Holliday 31509247de removed console logs, removed permission check for getting settings 2024-09-30 15:20:10 +08:00
Alexander Holliday 9ef259fb26 Merge pull request #896 from bluewave-labs/develop
Develop -> Master
2024-09-29 23:59:44 -07:00
Alexander Holliday b99cdaca4e Merge pull request #895 from bluewave-labs/fix/default-settings-state
set default url to empty string
2024-09-29 23:58:59 -07:00
Alex Holliday ca30dcc33c set default url to empty string 2024-09-30 14:58:20 +08:00
Alexander Holliday 4acb80076a Merge pull request #894 from bluewave-labs/feat/readme
Update readme
2024-09-29 22:31:56 -07:00
Alex Holliday 32eff933ca Update readme 2024-09-30 13:30:59 +08:00
Alexander Holliday e2cd6254ee Merge pull request #893 from bluewave-labs/feat/docker-quickstart
Update docker-compose dist version to use latest image
2024-09-29 22:04:15 -07:00
Alex Holliday c4adb445d6 Update docker-compose dist version to use latest image 2024-09-30 13:03:41 +08:00
Alexander Holliday 298202c9c2 Merge pull request #892 from bluewave-labs/develop
Develop -> Master
2024-09-29 21:27:43 -07:00
Alexander Holliday 2e63c4a189 Merge pull request #891 from bluewave-labs/fix/quickstart
Add docker images and compose for dist
2024-09-29 21:25:35 -07:00
Alex Holliday bc85062dd1 Add docker images and compose for dist 2024-09-30 12:24:53 +08:00
Alexander Holliday 9a33dffa50 Merge pull request #890 from bluewave-labs/feat/fe/in-app-settings
Feat/fe/in app settings
2024-09-29 20:34:33 -07:00
Alexander Holliday ac904aca8b Merge pull request #889 from bluewave-labs/feat/be/in-app-settings
Feat/be/in app settings
2024-09-29 20:33:15 -07:00
Alex Holliday 9b3085c84d Add default empty value for pagespeedApiKey in settings 2024-09-30 11:30:52 +08:00
Alex Holliday 6268b2573c remove jwtSecret from settings 2024-09-30 11:14:53 +08:00
Alex Holliday 6418efde2a Merge branch 'feat/be/in-app-settings' into feat/fe/in-app-settings 2024-09-30 10:08:45 +08:00
Alex Holliday 73ac91c201 Generate random JWT on first account signup if one is not supplied by env 2024-09-30 10:08:37 +08:00
Alex Holliday b6a7cfa7cc refactor getAppSettings endpoint to user settingsService as source of truth 2024-09-30 09:56:39 +08:00
Alex Holliday 806f530016 Merge branch 'feat/be/in-app-settings' into feat/fe/in-app-settings 2024-09-30 09:53:03 +08:00
Alex Holliday 114f4ffb05 refactor reloadSettings 2024-09-30 09:52:47 +08:00
Alexander Holliday c40408ae5e Merge pull request #888 from bluewave-labs/fix/fe/bump-dependencies
bump FE dependencies
2024-09-26 22:49:30 -07:00
Alex Holliday 453c8f6b12 bump FE dependencies 2024-09-27 13:46:50 +08:00
Alexander Holliday dbb0a6ffd9 Merge pull request #887 from mertssmnoglu/npm-vulnerabilities-886
Resolve known security vulnerabilities via 'npm audit fix'
2024-09-26 22:44:35 -07:00
Alex Holliday b8984c0160 Restrict access to advanced settings page 2024-09-27 13:38:57 +08:00
Alex Holliday 43a56e1de1 Fix setting name 2024-09-27 13:17:28 +08:00
Alex Holliday 7fc5f2d025 Merge branch 'feat/be/in-app-settings' into feat/fe/in-app-settings 2024-09-27 13:10:40 +08:00
Alex Holliday f4bbbc5fa9 fix conditional loadingof settings 2024-09-27 13:10:29 +08:00
Alex Holliday 6037d13106 Merge branch 'feat/be/in-app-settings' into feat/fe/in-app-settings 2024-09-27 13:02:20 +08:00
Alex Holliday c3edbb779a Modify settingsService to load from env by default 2024-09-27 13:02:01 +08:00
Alex Holliday d418e2f1f8 Add remainder of settings 2024-09-27 12:22:50 +08:00
Alex Holliday 5c4d094da7 update settings 2024-09-27 12:13:39 +08:00
Alex Holliday 488ba717ac Add advacned settings page 2024-09-27 10:55:37 +08:00
Mert Şişmanoğlu 1cae407f65 chore(deps): resolve known security vulnerabilities via 'npm audit fix' (#886) 2024-09-26 23:23:07 +03:00
Alex Holliday 179e95eefe add redux state for settings 2024-09-26 16:18:25 +08:00
Alex Holliday 97419b436c Add reload settings method, reload settings when DB updated 2024-09-26 15:45:54 +08:00
Alex Holliday 0832e8438a remove console log 2024-09-26 15:38:02 +08:00
Alex Holliday 11b97ab3b4 add update settings method, remove all env var references possible 2024-09-26 14:37:11 +08:00
Alex Holliday 992bf099fa Add app settings model, routes, db ops, service, use settings for issueToken 2024-09-26 13:41:50 +08:00
Alexander Holliday b2bc441c7f Merge pull request #885 from bluewave-labs/fix/delete-super-admin
Update delete user to handle superadmin case
2024-09-25 20:51:40 -07:00
Alex Holliday edd9b5a6aa Update delete user to handle superadmin case 2024-09-26 11:50:50 +08:00
Gorkem Cetin (BWL) f29dc3f038 Update README.md 2024-09-25 23:39:56 -04:00
Alexander Holliday 5ec032466b Merge pull request #884 from bluewave-labs/fix/fe-prop-errors
Fix/fe prop errors
2024-09-25 20:09:10 -07:00
Alexander Holliday fe340e8661 Merge pull request #883 from bluewave-labs/fix/refactor-usermodel-name
user -> User
2024-09-25 20:08:19 -07:00
Alex Holliday 38ee162f61 improve error handling in network service, fix username form errors 2024-09-26 11:01:38 +08:00
Alex Holliday 09fb5595ab Fix proptype errors, remove old alert reference 2024-09-26 10:42:03 +08:00
Alex Holliday 888f644c7c user -> User 2024-09-26 10:28:48 +08:00
Alexander Holliday bfc01de28c Merge pull request #882 from bluewave-labs/develop
Develop -> Master
2024-09-25 00:18:25 -07:00
Alexander Holliday 7454e8812e Merge pull request #881 from bluewave-labs/feat/register-security
Feat/register security
2024-09-25 00:16:28 -07:00
Alex Holliday ffd193437e add comments 2024-09-25 15:14:22 +08:00
Alex Holliday 5dbd8378e2 require invite token to register except for initial registration 2024-09-25 15:12:07 +08:00
Alex Holliday 4e8675df45 Remove API endpoints from docs, link to swagger docs 2024-09-25 14:07:33 +08:00
Alex Holliday e2df0e7c6c Fix typo 2024-09-25 13:46:55 +08:00
Alexander Holliday 3b9ffa7804 Merge pull request #880 from bluewave-labs/develop
Develop -> Master
2024-09-24 22:42:56 -07:00
Alexander Holliday 7527a7eb4f Merge pull request #879 from bluewave-labs/feat/swagger-config
Add corrected bluewave server to swagger docs
2024-09-24 22:42:20 -07:00
Alex Holliday 7996210852 Add corrected bluewave server to swagger docs 2024-09-25 13:41:19 +08:00
Alexander Holliday 8f10f5fa3e Merge pull request #878 from bluewave-labs/develop
Develop -> Master
2024-09-24 22:37:43 -07:00
Alexander Holliday 43cbb07b45 Merge pull request #877 from bluewave-labs/feat/swagger-config
Add bluewave server to swagger config
2024-09-24 22:37:12 -07:00
Alex Holliday 5c4174d3ac Add bluewave server to swagger config 2024-09-25 13:36:35 +08:00
Alexander Holliday 47e0c9d54f Merge pull request #876 from bluewave-labs/develop
Develop -> Master
2024-09-24 22:18:09 -07:00
Alexander Holliday d642b93455 Merge pull request #875 from bluewave-labs/fix/add-dependency
Add swagger-ui dependency
2024-09-24 22:17:13 -07:00
Alex Holliday 06057671b4 Add swagger-ui dependency 2024-09-25 13:16:27 +08:00
Alexander Holliday 711e866702 Merge pull request #874 from bluewave-labs/develop
Develop -> Master
2024-09-24 22:08:18 -07:00
Alexander Holliday 20c27ee3e7 Merge pull request #873 from bluewave-labs/feat/swagger-ui
Feat/swagger UI
2024-09-24 22:07:28 -07:00
Alex Holliday 825777c2a8 Update nginx conf for api docs 2024-09-25 13:03:03 +08:00
Alex Holliday d55119c28d Add job queue to docs 2024-09-25 12:55:31 +08:00
Alex Holliday 35d2d8301e Add maintenance window routes to docs 2024-09-25 12:11:18 +08:00
Alex Holliday f2407ac5fa Remove unused pagespeed modules 2024-09-25 11:52:39 +08:00
Alex Holliday 4ed7d426e8 Merge branch 'fix/remove-unused-alert-module' into feat/swagger-ui 2024-09-25 11:50:25 +08:00
Alex Holliday d849374225 remove unused routes from docs 2024-09-25 11:49:28 +08:00
Alex Holliday 115831ca34 Remove unsued alert module 2024-09-25 11:41:50 +08:00
Alex Holliday 36014fada2 refactor ttl endpoint path, add /team/ttl to docs 2024-09-25 11:37:52 +08:00
Alex Holliday c44152de93 Add deleteChecksByTeam route 2024-09-25 11:30:59 +08:00
Alex Holliday 885a89e36b Add getChecksByTeam route 2024-09-25 11:29:20 +08:00
Alex Holliday 05f1b2b6b4 Add getCheck createCheck routes to docs 2024-09-25 11:23:36 +08:00
Alex Holliday 7a78a6d6b0 Add pause and add demo routes to docs 2024-09-25 11:10:05 +08:00
Alex Holliday 24863a2fb7 Add put to monitors/:monitorId doc 2024-09-25 11:03:50 +08:00
Alex Holliday 17f2039fc2 Add delete to monitors/:monitorId doc 2024-09-25 10:57:44 +08:00
Alex Holliday fe97390e53 Added put and delete to monitors docs 2024-09-25 10:54:56 +08:00
Alexander Holliday 4ea66541fd Merge pull request #872 from bluewave-labs/develop
Develop -> Master
2024-09-24 19:12:12 -07:00
Alexander Holliday 7ee05e8af2 Merge pull request #871 from bluewave-labs/fix/pagespeed-demo-rendering
Add conditional rendering to PageSpeed pages for non admin users
2024-09-24 19:11:40 -07:00
Alex Holliday e314def79a Add conditional rendering to PageSpeed pages for non admin users 2024-09-25 10:09:56 +08:00
Alex Holliday db4dbc6a5f Add getMonitorsAndSummaryByTeamID to docs 2024-09-24 15:38:27 +08:00
Alex Holliday 1697e853e6 Add getMonitorById to docs 2024-09-24 15:27:01 +08:00
Alex Holliday fd483b1857 Add get certificate by monitor ID to docs 2024-09-24 15:23:40 +08:00
Alex Holliday bd03964ab6 Add get monitor stats by ID to docs 2024-09-24 15:20:45 +08:00
Alex Holliday 8cccc58d39 Merge remote-tracking branch 'upstream/develop' into feat/swagger-ui 2024-09-24 15:15:03 +08:00
Alex Holliday 53f2361a10 add get all monitors to docs 2024-09-24 15:12:38 +08:00
Alex Holliday c9ab3d9fde Add security to invite docs 2024-09-24 15:09:05 +08:00
Alex Holliday 0a49a070db Add invite to swagger docs 2024-09-24 15:06:24 +08:00
Alex Holliday 7582dfdcc3 Update auth endpoints 2024-09-24 14:58:46 +08:00
Alex Holliday d2ab33492a Update auth endpoints 2024-09-24 13:56:58 +08:00
Daniel Cojocea 9fba9e939e Added Calendar field 2024-09-23 20:31:08 -04:00
Daniel Cojocea 577bc9447d Added fields 2024-09-18 21:22:22 -04:00
Daniel Cojocea 6c09606e40 Merge latest changes 2024-09-18 19:51:56 -04:00
Daniel Cojocea ee30f44062 Added basic layout 2024-09-18 19:50:01 -04:00
Daniel Cojocea 91a2a72bf2 Purged outdated code 2024-09-18 19:39:33 -04:00
164 changed files with 9410 additions and 4744 deletions

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
.vite
.vscode
*.sh
!uptime.sh
!uptime.sh
*.idea
+87 -83
View File
@@ -1759,9 +1759,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
"integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
"cpu": [
"arm"
],
@@ -1772,9 +1772,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
"integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
"cpu": [
"arm64"
],
@@ -1785,9 +1785,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
"integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
"cpu": [
"arm64"
],
@@ -1798,9 +1798,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
"integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
"cpu": [
"x64"
],
@@ -1811,9 +1811,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
"integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
"cpu": [
"arm"
],
@@ -1824,9 +1824,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
"integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
"cpu": [
"arm"
],
@@ -1837,9 +1837,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
"integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
"cpu": [
"arm64"
],
@@ -1850,9 +1850,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
"integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
"cpu": [
"arm64"
],
@@ -1863,9 +1863,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
"integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
"cpu": [
"ppc64"
],
@@ -1876,9 +1876,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
"integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
"cpu": [
"riscv64"
],
@@ -1889,9 +1889,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
"integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
"cpu": [
"s390x"
],
@@ -1902,9 +1902,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
"integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
"cpu": [
"x64"
],
@@ -1915,9 +1915,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
"integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
"cpu": [
"x64"
],
@@ -1928,9 +1928,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
"integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
"cpu": [
"arm64"
],
@@ -1941,9 +1941,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
"integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
"cpu": [
"ia32"
],
@@ -1954,9 +1954,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
"integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
"cpu": [
"x64"
],
@@ -5249,9 +5249,9 @@
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"license": "ISC"
},
"node_modules/picomatch": {
@@ -5277,9 +5277,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -5297,8 +5297,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -5678,9 +5678,9 @@
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
"integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
@@ -5693,22 +5693,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.18.1",
"@rollup/rollup-android-arm64": "4.18.1",
"@rollup/rollup-darwin-arm64": "4.18.1",
"@rollup/rollup-darwin-x64": "4.18.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
"@rollup/rollup-linux-arm-musleabihf": "4.18.1",
"@rollup/rollup-linux-arm64-gnu": "4.18.1",
"@rollup/rollup-linux-arm64-musl": "4.18.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
"@rollup/rollup-linux-riscv64-gnu": "4.18.1",
"@rollup/rollup-linux-s390x-gnu": "4.18.1",
"@rollup/rollup-linux-x64-gnu": "4.18.1",
"@rollup/rollup-linux-x64-musl": "4.18.1",
"@rollup/rollup-win32-arm64-msvc": "4.18.1",
"@rollup/rollup-win32-ia32-msvc": "4.18.1",
"@rollup/rollup-win32-x64-msvc": "4.18.1",
"@rollup/rollup-android-arm-eabi": "4.22.4",
"@rollup/rollup-android-arm64": "4.22.4",
"@rollup/rollup-darwin-arm64": "4.22.4",
"@rollup/rollup-darwin-x64": "4.22.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
"@rollup/rollup-linux-arm64-musl": "4.22.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
"@rollup/rollup-linux-x64-gnu": "4.22.4",
"@rollup/rollup-linux-x64-musl": "4.22.4",
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
"@rollup/rollup-win32-x64-msvc": "4.22.4",
"fsevents": "~2.3.2"
}
},
@@ -5887,9 +5887,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6266,14 +6266,14 @@
}
},
"node_modules/vite": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
"rollup": "^4.13.0"
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -6292,6 +6292,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -6309,6 +6310,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
+41 -11
View File
@@ -19,13 +19,14 @@ import SetNewPassword from "./Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Monitors/Details";
import AdvancedSettings from "./Pages/AdvancedSettings";
import Maintenance from "./Pages/Maintenance";
import withAdminCheck from "./HOC/withAdminCheck";
import withAdminProp from "./HOC/withAdminProp";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "./Pages/PageSpeed/Details";
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
import { ThemeProvider } from "@emotion/react";
@@ -33,16 +34,37 @@ import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { useSelector } from "react-redux";
import { CssBaseline } from "@mui/material";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
const MonitorsWithAdminProp = withAdminProp(Monitors);
const DetailsWithAdminProp = withAdminProp(Details);
const MonitorDetailsWithAdminProp = withAdminProp(Details);
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
const SettingsWithAdminProp = withAdminProp(Settings);
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
if (authToken) {
dispatch(getAppSettings({ authToken }));
}
}, [dispatch, authToken]);
// Cleanup
useEffect(() => {
return () => {
logger.cleanup();
networkService.cleanup();
};
}, []);
return (
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
@@ -64,7 +86,7 @@ function App() {
/>
<Route
path="/monitors/:monitorId/"
element={<ProtectedRoute Component={DetailsWithAdminProp} />}
element={<ProtectedRoute Component={MonitorDetailsWithAdminProp} />}
/>
<Route
path="/monitors/configure/:monitorId/"
@@ -83,18 +105,24 @@ function App() {
path="integrations"
element={<ProtectedRoute Component={Integrations} />}
/>
{/* <Route
<Route
path="maintenance"
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
/> */}
{/* <Route
path="/maintenance/create"
/>
<Route
path="/maintenance/create/:maintenanceWindowId?"
element={<CreateNewMaintenanceWindow />}
/> */}
/>
<Route
path="settings"
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
/>
<Route
path="advanced-settings"
element={
<ProtectedRoute Component={AdvancedSettingsWithAdminProp} />
}
/>
<Route
path="account/profile"
element={<ProtectedRoute Component={Account} open="profile" />}
@@ -117,7 +145,9 @@ function App() {
/>
<Route
path="pagespeed/:monitorId"
element={<ProtectedRoute Component={PageSpeedDetails} />}
element={
<ProtectedRoute Component={PageSpeedDetailsWithAdminProp} />
}
/>
<Route
path="pagespeed/configure/:monitorId"
+10 -1
View File
@@ -16,6 +16,7 @@ import "./index.css";
* @param {Object} props
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
* @param {string} props.id - ID of the input field.
* @param {string} props.name - Name of the input field.
* @param {string} [props.label] - Label for the input field.
* @param {boolean} [props.https] - Indicates if it should display http or https.
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
@@ -27,6 +28,7 @@ import "./index.css";
* @param {function} props.onChange - Function called on input change.
* @param {string} [props.error] - Error message to display for the input field.
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
* @param {boolean} [props.hidden] - Indicates if the input field is hidden.
* @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
*/
@@ -35,6 +37,7 @@ const Field = forwardRef(
{
type = "text",
id,
name,
label,
https,
isRequired,
@@ -47,6 +50,7 @@ const Field = forwardRef(
onInput,
error,
disabled,
hidden,
},
ref
) => {
@@ -107,6 +111,7 @@ const Field = forwardRef(
)}
<TextField
type={type === "password" ? (isVisible ? "text" : type) : type}
name={name}
id={id}
autoComplete={autoComplete}
placeholder={placeholder}
@@ -136,7 +141,9 @@ const Field = forwardRef(
borderBottomLeftRadius: theme.shape.borderRadius,
},
}
: {}
: {
display: hidden ? "none" : "",
}
}
InputProps={{
startAdornment: type === "url" && (
@@ -213,6 +220,7 @@ Field.propTypes = {
"number",
]),
id: PropTypes.string.isRequired,
name: PropTypes.string,
label: PropTypes.string,
https: PropTypes.bool,
isRequired: PropTypes.bool,
@@ -225,6 +233,7 @@ Field.propTypes = {
onInput: PropTypes.func,
error: PropTypes.string,
disabled: PropTypes.bool,
hidden: PropTypes.bool,
};
export default Field;
+82 -37
View File
@@ -1,5 +1,12 @@
import PropTypes from "prop-types";
import { Box, ListItem, Autocomplete, TextField } from "@mui/material";
import {
Box,
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
@@ -15,21 +22,52 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
* @param {Object} props.sx - Additional styles to apply to the component
* @returns {JSX.Element} The rendered Search component
*/
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
};
const Search = ({
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
}) => {
const theme = useTheme();
return (
<Autocomplete
multiple={multiple}
id={id}
inputValue={value}
value={value}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
@@ -38,45 +76,45 @@ const Search = ({
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={options}
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<TextField
{...params}
placeholder="Type to search"
InputProps={{
...params.InputProps,
startAdornment: (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
<Stack>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
InputProps={{
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.light,
},
}}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.text}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
)}
filterOptions={(options, { inputValue }) => {
const filtered = options.filter((option) =>
@@ -103,7 +141,8 @@ const Search = ({
: {}
}
>
{option[filteredBy]}
{option[filteredBy] +
(secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
</ListItem>
);
}}
@@ -139,12 +178,18 @@ const Search = ({
Search.propTypes = {
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
};
export default Search;
@@ -34,6 +34,7 @@ import "./index.css";
*
* <Select
* id="frequency-id"
* name="my-name"
* label="Check frequency"
* placeholder="Select frequency"
* value={value}
@@ -51,6 +52,7 @@ const Select = ({
items,
onChange,
sx,
name = "",
}) => {
const theme = useTheme();
const itemStyles = {
@@ -77,6 +79,7 @@ const Select = ({
value={value}
onChange={onChange}
displayEmpty
name={name}
inputProps={{ id: id }}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{ disableScrollLock: true }}
@@ -127,6 +130,7 @@ const Select = ({
Select.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,
+1 -1
View File
@@ -55,7 +55,7 @@ const menu = [
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
// { name: "Status pages", path: "status", icon: <StatusPages /> },
// { name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{
name: "Account",
@@ -1,5 +1,5 @@
import TabPanel from "@mui/lab/TabPanel";
import React, { useState } from "react";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -116,6 +116,14 @@ const PasswordPanel = () => {
<Box flex={0.9}>
<Typography component="h1">Current password</Typography>
</Box>
<Field
type="text"
id="hidden-username"
name="username"
autoComplete="username"
hidden={true}
value=""
/>
<Field
type="password"
id="edit-current-password"
@@ -0,0 +1,117 @@
import { networkService } from "../../main";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const initialState = {
isLoading: false,
apiBaseUrl: "",
logLevel: "debug",
};
export const getAppSettings = createAsyncThunk(
"settings/getSettings",
async (data, thunkApi) => {
try {
const res = await networkService.getAppSettings({
authToken: data.authToken,
});
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updateAppSettings = createAsyncThunk(
"settings/updateSettings",
async ({ settings, authToken }, thunkApi) => {
networkService.setBaseUrl(settings.apiBaseUrl);
try {
const parsedSettings = {
apiBaseUrl: settings.apiBaseUrl,
logLevel: settings.logLevel,
clientHost: settings.clientHost,
jwtSecret: settings.jwtSecret,
dbType: settings.dbType,
dbConnectionString: settings.dbConnectionString,
redisHost: settings.redisHost,
redisPort: settings.redisPort,
jwtTTL: settings.jwtTTL,
pagespeedApiKey: settings.pagespeedApiKey,
systemEmailHost: settings.systemEmailHost,
systemEmailPort: settings.systemEmailPort,
systemEmailAddress: settings.systemEmailAddress,
systemEmailPassword: settings.systemEmailPassword,
};
const res = await networkService.updateAppSettings({
settings: parsedSettings,
authToken,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const handleGetSettingsFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.apiBaseUrl = action.payload.data.apiBaseUrl;
state.logLevel = action.payload.data.logLevel;
};
const handleGetSettingsRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
};
const handleUpdateSettingsFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.apiBaseUrl = action.payload.data.apiBaseUrl;
state.logLevel = action.payload.data.logLevel;
};
const handleUpdateSettingsRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to update settings.";
};
const settingsSlice = createSlice({
name: "settings",
initialState,
extraReducers: (builder) => {
builder
.addCase(getAppSettings.pending, (state) => {
state.isLoading = true;
})
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
builder
.addCase(updateAppSettings.pending, (state) => {
state.isLoading = true;
})
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
},
});
export default settingsSlice.reducer;
+3
View File
@@ -9,6 +9,9 @@ const initialState = {
team: {
rowsPerPage: 5,
},
maintenance: {
rowsPerPage: 5,
},
sidebar: {
collapsed: false,
},
Whitespace-only changes.
+265
View File
@@ -0,0 +1,265 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import "./index.css";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { ConfigBox } from "../Settings/styled";
import { useNavigate } from "react-router";
import {
getAppSettings,
updateAppSettings,
} from "../../Features/Settings/settingsSlice";
import { useState, useEffect } from "react";
import Select from "../../Components/Inputs/Select";
const AdvancedSettings = ({ isAdmin }) => {
const navigate = useNavigate();
useEffect(() => {
if (!isAdmin) {
navigate("/");
}
}, [navigate, isAdmin]);
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings);
const [localSettings, setLocalSettings] = useState({
apiBaseUrl: "",
logLevel: "debug",
systemEmailHost: "",
systemEmailPort: "",
systemEmailAddress: "",
systemEmailPassword: "",
jwtTTL: "",
dbType: "",
redisHost: "",
redisPort: "",
pagespeedApiKey: "",
});
useEffect(() => {
const getSettings = async () => {
const action = await dispatch(getAppSettings({ authToken }));
if (action.payload.success) {
setLocalSettings(action.payload.data);
} else {
createToast({ body: "Failed to get settings" });
}
};
getSettings();
}, [authToken, dispatch]);
const logItems = [
{ _id: 1, name: "none" },
{ _id: 2, name: "debug" },
{ _id: 3, name: "error" },
{ _id: 4, name: "warn" },
];
const logItemLookup = {
none: 1,
debug: 2,
error: 3,
warn: 4,
};
const handleLogLevel = (e) => {
const id = e.target.value;
const newLogLevel = logItems.find((item) => item._id === id).name;
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
};
const handleChange = (event) => {
const { value, id } = event.target;
setLocalSettings({ ...localSettings, [id]: value });
};
const handleSave = async () => {
const action = await dispatch(
updateAppSettings({ settings: localSettings, authToken })
);
let body = "";
if (action.payload.success) {
console.log(action.payload.data);
setLocalSettings(action.payload.data);
body = "Settings saved successfully";
} else {
body = "Failed to save settings";
}
createToast({ body });
};
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for
sending system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="systemEmailHost"
label="Email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
/>
<Field
type="number"
id="systemEmailPort"
label="System email address"
name="systemEmailPort"
value={localSettings.systemEmailPort.toString()}
onChange={handleChange}
/>
<Field
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
/>
<Field
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="jwtTTL"
label="JWT time to live"
name="jwtTTL"
value={localSettings.jwtTTL}
onChange={handleChange}
/>
<Field
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
/>
<Field
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
/>
<Field
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort.toString()}
onChange={handleChange}
/>
<Field
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
AdvancedSettings.propTypes = {
isAdmin: PropTypes.bool,
};
export default AdvancedSettings;
+3 -2
View File
@@ -596,6 +596,7 @@ const Register = ({ isSuperAdmin }) => {
let registerForm = {
...form,
role: isSuperAdmin ? ["superadmin"] : form.role,
inviteToken: token ? token : "", // Add the token to the request for verification
};
let error = validateForm(registerForm, {
context: { password: form.password },
@@ -608,8 +609,8 @@ const Register = ({ isSuperAdmin }) => {
delete registerForm.confirm;
const action = await dispatch(register(registerForm));
if (action.payload.success) {
const token = action.payload.data;
localStorage.setItem("token", token);
const authToken = action.payload.data;
localStorage.setItem("token", authToken);
navigate("/");
createToast({
body: "Welcome! Your account was created successfully.",
@@ -0,0 +1,3 @@
.create-maintenance button {
height: 35px;
}
@@ -0,0 +1,570 @@
import { Box, Button, duration, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { ConfigBox } from "./styled";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import LoadingButton from "@mui/lab/LoadingButton";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
import Field from "../../../Components/Inputs/Field";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
import "./index.css";
import Search from "../../../Components/Inputs/Search";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../Utils/timeUtils";
import { useNavigate, useParams } from "react-router-dom";
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
const repeatConfig = [
{ _id: 0, name: "Don't repeat", value: "none" },
{
_id: 1,
name: "Repeat daily",
value: "daily",
},
{ _id: 2, name: "Repeat weekly", value: "weekly" },
];
const durationConfig = [
{ _id: 0, name: "seconds" },
{ _id: 1, name: "minutes" },
{ _id: 2, name: "hours" },
{
_id: 3,
name: "days",
},
];
const getValueById = (config, id) => {
const item = config.find((config) => config._id === id);
return item ? (item.value ? item.value : item.name) : null;
};
const getIdByValue = (config, name) => {
const item = config.find((config) => {
if (config.value) {
return config.value === name;
} else {
return config.name === name;
}
});
return item ? item._id : null;
};
const CreateMaintenance = () => {
const { maintenanceWindowId } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState([]);
const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [form, setForm] = useState({
repeat: "none",
startDate: dayjs(),
startTime: dayjs(),
duration: "",
durationUnit: "seconds",
name: "",
monitors: [],
});
const [errors, setErrors] = useState({});
useEffect(() => {
const fetchMonitors = async () => {
setIsLoading(true);
try {
const response = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: user.teamId,
limit: -1,
types: ["http", "ping", "pagespeed"],
});
const monitors = response.data.data.monitors;
setMonitors(monitors);
if (maintenanceWindowId === undefined) {
return;
}
const res = await networkService.getMaintenanceWindowById({
authToken: authToken,
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, user]);
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error) {
updatedErrors[id] = error.details[0].message;
} else {
delete updatedErrors[id];
}
return updatedErrors;
};
const handleSearch = (value) => {
setSearch(value);
};
const handleSelectMonitors = (monitors) => {
setForm({ ...form, monitors });
const { error } = maintenanceWindowValidation.validate(
{ monitors },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleFormChange = (key, value) => {
setForm({ ...form, [key]: value });
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleTimeChange = (key, newTime) => {
setForm({ ...form, [key]: newTime });
const { error } = maintenanceWindowValidation.validate(
{ [key]: newTime },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleSubmit = async () => {
const { error } = maintenanceWindowValidation.validate(form, {
abortEarly: false,
});
// If errors, return early
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
logger.error(error);
return;
}
// Build timestamp for maintenance window from startDate and startTime
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
// Get repeat value in milliseconds
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { authToken: authToken, maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
try {
setIsLoading(true);
await request;
createToast({
body: "Successfully created maintenance window",
});
navigate("/maintenance");
} catch (error) {
createToast({
body: `Failed to ${
maintenanceWindowId === undefined ? "create" : "edit"
} maintenance window`,
});
logger.error(error);
} finally {
setIsLoading(false);
}
};
return (
<Box className="create-maintenance">
<Breadcrumbs
list={[
{ name: "maintenance", path: "/maintenance" },
{
name: maintenanceWindowId === undefined ? "create" : "edit",
path: `/maintenance/create`,
},
]}
/>
<Stack
component="form"
noValidate
spellCheck="false"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Box>
<Typography component="h1" variant="h1">
<Typography component="span" fontSize="inherit">
{`${maintenanceWindowId === undefined ? "Create a" : "Edit"}`}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
maintenance{" "}
</Typography>
<Typography component="span" fontSize="inherit">
window
</Typography>
</Typography>
<Typography component="h2" variant="body2" fontSize={14}>
Your pings won&apos;t be sent during this time frame
</Typography>
</Box>
<ConfigBox direction="row">
<Typography component="h2" variant="h2">
General Settings
</Typography>
<Box
px={theme.spacing(14)}
borderLeft={1}
borderLeftColor={theme.palette.border.light}
>
<Select
id="repeat"
name="maintenance-repeat"
label="Maintenance Repeat"
value={getIdByValue(repeatConfig, form.repeat)}
onChange={(event) => {
handleFormChange(
"repeat",
getValueById(repeatConfig, event.target.value)
);
}}
items={repeatConfig}
/>
<Stack gap={theme.spacing(2)} mt={theme.spacing(16)}>
<Typography component="h3">Date</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
id="startDate"
disablePast
disableHighlightToday
value={form.startDate}
slots={{ openPickerIcon: CalendarIcon }}
slotProps={{
switchViewButton: { sx: { display: "none" } },
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pr: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
},
inputAdornment: { sx: { ml: 0, px: 3 } },
openPickerButton: {
sx: {
py: 0,
mr: 0,
"& path": {
stroke: theme.palette.other.icon,
strokeWidth: 1.1,
},
"&:hover": { backgroundColor: "transparent" },
},
},
}}
sx={{}}
onChange={(newDate) => {
handleTimeChange("startDate", newDate);
}}
error={errors["startDate"]}
/>
</LocalizationProvider>
</Stack>
</Box>
</ConfigBox>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Start time
</Typography>
<Typography>
All dates and times are in GMT+0 time zone.
</Typography>
</Box>
<Stack direction="row">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
</Stack>
</Stack>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Duration
</Typography>
</Box>
<Stack direction="row" spacing={theme.spacing(8)}>
<Field
type="number"
id="duration"
value={form.duration}
onChange={(event) => {
handleFormChange("duration", event.target.value);
}}
error={errors["duration"]}
/>
<Select
id="durationUnit"
value={getIdByValue(durationConfig, form.durationUnit)}
items={durationConfig}
onChange={(event) => {
handleFormChange(
"durationUnit",
getValueById(durationConfig, event.target.value)
);
}}
error={errors["durationUnit"]}
/>
</Stack>
</Stack>
</ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
fontSize={16}
my={theme.spacing(6)}
>
Monitor related settings
</Typography>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Friendly name
</Typography>
</Box>
<Box>
<Field
id="name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"]}
/>
</Box>
</Stack>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Add monitors
</Typography>
</Box>
<Box>
<Search
id={"monitors"}
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Box>
</Stack>
</ConfigBox>
</Box>
<Box ml="auto" display="inline-block">
<Button
variant="contained"
color="secondary"
onClick={() => navigate("/maintenance")}
sx={{ mr: theme.spacing(6) }}
>
Cancel
</Button>
<LoadingButton
loading={isLoading}
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={false}
>
{`${
maintenanceWindowId === undefined
? "Create maintenance"
: "Edit maintenance"
}`}
</LoadingButton>
</Box>
</Stack>
</Box>
);
};
export default CreateMaintenance;
@@ -0,0 +1,25 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": { padding: theme.spacing(14) },
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
flex: 0.6,
},
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
flex: 1,
},
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
"& > .MuiStack-root > div:last-of-type": {
paddingLeft: theme.spacing(14),
},
"& h2": { fontSize: 13.5, fontWeight: 500 },
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& h3": { fontWeight: 500 },
}));
@@ -1,71 +0,0 @@
.maintenance-options .MuiStack-root .MuiStack-root.select-wrapper,
.maintenance-options .MuiStack-root .field {
width: 60%;
max-width: 380px;
}
.date-picker .MuiOutlinedInput-root,
.time-picker .MuiOutlinedInput-root {
height: 34px;
font-size: var(--env-var-font-size-medium);
}
.date-picker .MuiOutlinedInput-root {
width: 140px;
}
.time-picker .MuiOutlinedInput-root {
width: 90px;
}
.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
width: 20px;
}
.duration-config .field-text .MuiTextField-root,
.maintenance-options .duration-config .field {
width: 70px;
min-width: 70px;
max-width: 70px;
}
.duration-config > .MuiStack-root.select-wrapper {
width: 100px;
}
.duration-config
> .MuiStack-root.select-wrapper
> .MuiInputBase-root:has(> .MuiSelect-select) {
width: 100px;
min-width: 100px;
}
.duration-config > .select-wrapper .select-component > .MuiSelect-select {
text-transform: none;
}
.maintenance-options .add-monitors-fields .field.field-text {
width: 100%;
}
.MuiPopover-paper.MuiMenu-paper.select-dropdown
.MuiMenu-list
.MuiButtonBase-root {
text-transform: none;
}
.date-picker
.MuiOutlinedInput-root
.MuiInputAdornment-outlined
.MuiIconButton-root {
outline: none;
&:hover {
background-color: transparent;
}
}
.create-maintenance-window .MuiStack-root .MuiStack-root > button {
width: 110px;
height: 34px;
}
@@ -1,298 +0,0 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import "./index.css";
import { useState } from "react";
import Back from "../../../assets/icons/left-arrow-long.svg?react";
import Select from "../../../Components/Inputs/Select";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import Field from "../../../Components/Inputs/Field";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../Utils/toastUtils";
import { useTheme } from "@emotion/react";
const directory = {
title: "Create a maintenance window",
description: "Your pings won’t be sent in this time frame.",
};
const repeatOptions = [
{ _id: 1, name: "Don't repeat" },
{ _id: 2, name: "Repeat daily" },
{ _id: 3, name: "Repeat weekly" },
];
const configOptionTitle = (title, description) => {
return (
<Stack width="40%" gap={1}>
<Typography
style={{
fontWeight: 600,
fontSize: "var(--env-var-font-size-medium)",
}}
>
{title}
</Typography>
{description && (
<Typography
style={{
fontSize: "var(--env-var-font-size-small)",
}}
>
{description}
</Typography>
)}
</Stack>
);
};
const durationOptions = [
{ _id: "minutes", name: "minutes" },
{ _id: "hours", name: "hours" },
{ _id: "days", name: "days" },
];
const CreateNewMaintenanceWindow = () => {
const theme = useTheme();
const [values, setValues] = useState({
repeat: 1,
date: dayjs(),
startTime: dayjs(),
duration: "60",
unit: "minutes",
displayName: "",
AddMonitors: "",
});
const [errors, setErrors] = useState({});
const handleChange = (event, name) => {
const { value } = event.target;
setValues((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async () => {
const data = {
repeat: values.repeat,
date: values.date.format("YYYY-MM-DD"),
startTime: values.startTime.format("HH:mm"),
duration: values.duration,
unit: values.unit,
displayName: values.displayName,
addMonitors: values.AddMonitors,
};
const { error } = maintenanceWindowValidation.validate(data, {
abortEarly: false,
});
logger.log("error: ", error);
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data",
});
logger.error("Validation errors:", error.details);
}
logger.log("Submitting data: ", data);
};
const configOptions = [
{
title: "Repeat",
component: (
<Select
onChange={(e) => handleChange(e, "repeat")}
id="repeat-mode"
items={repeatOptions}
value={values.repeat}
/>
),
},
{
title: "Date",
component: (
<LocalizationProvider
className="date-localization-provider"
dateAdapter={AdapterDayjs}
>
<DatePicker
className="date-picker"
defaultValue={values.date}
onChange={(date) =>
handleChange({ target: { value: date } }, "date")
}
/>
</LocalizationProvider>
),
},
{
title: "Start time",
component: (
<LocalizationProvider
className="time-localization-provider"
dateAdapter={AdapterDayjs}
>
<MobileTimePicker
className="time-picker"
defaultValue={values.startTime}
onChange={(time) =>
handleChange({ target: { value: time } }, "startTime")
}
/>
</LocalizationProvider>
),
},
{
title: "Duration",
component: (
<Stack
className="duration-config"
gap={theme.spacing(5)}
direction="row"
>
<Field
id="duration-value"
placeholder="60"
onChange={(e) => handleChange(e, "duration")}
value={values.duration}
error={errors.duration}
type="number"
/>
<Select
onChange={(e) => handleChange(e, "unit")}
id="duration-unit"
items={durationOptions}
value={values.unit}
/>
</Stack>
),
},
{
title: "Display name",
component: (
<Field
id="display-name"
placeholder="Maintanence at __ : __ for ___ minutes"
value={values.displayName}
onChange={(e) => handleChange(e, "displayName")}
error={errors.displayName}
/>
),
},
{
title: "Add monitors",
component: (
<Stack
className="add-monitors-fields"
sx={{ width: "60%", maxWidth: "380px" }}
gap={theme.spacing(5)}
>
<Field
id="add-monitors"
placeholder="Start typing to search for current monitors"
value={values.AddMonitors}
onChange={(e) => handleChange(e, "AddMonitors")}
error={errors.addMonitors}
/>
<Typography
sx={{
width: "fit-content",
fontSize: "var(--env-var-font-size-small)",
borderBottom: `1px dashed ${theme.palette.primary.main}`,
paddingBottom: "4px",
}}
>
Add all monitors to this maintenance window
</Typography>
</Stack>
),
},
];
return (
<div className="create-maintenance-window">
<Stack gap={theme.spacing(10)}>
<Button
variant="contained"
color="secondary"
sx={{
width: "100px",
height: "30px",
gap: "10px",
}}
>
<Back />
Back
</Button>
<Box>
<Typography
sx={{
fontSize: "var(--env-var-font-size-large)",
fontWeight: 600,
color: theme.palette.text.secondary,
}}
>
{directory.title}
</Typography>
<Typography sx={{ fontSize: "var(--env-var-font-size-medium)" }}>
{directory.description}
</Typography>
</Box>
<Stack
className="maintenance-options"
gap={theme.spacing(20)}
paddingY={theme.spacing(15)}
paddingX={theme.spacing(20)}
sx={{
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.main,
}}
>
{configOptions.map((item, index) => (
<Stack key={index} display="-webkit-inline-box">
{item.title && configOptionTitle(item.title, item.description)}
{item.component && item.component}
</Stack>
))}
</Stack>
<Stack justifyContent="end" direction="row" marginTop={3}>
<Button
variant="text"
color="info"
sx={{
"&:hover": {
backgroundColor: "transparent",
},
}}
>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Create
</Button>
</Stack>
</Stack>
</div>
);
};
export default CreateNewMaintenanceWindow;
@@ -0,0 +1,229 @@
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import {
Button,
IconButton,
Menu,
MenuItem,
Modal,
Stack,
Typography,
} from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { logger } from "../../../../Utils/Logger";
import Settings from "../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
maintenanceWindow;
const { authToken } = useSelector((state) => state.auth);
const [anchorEl, setAnchorEl] = useState(null);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
try {
setIsLoading(true);
await networkService.deleteMaintenanceWindow({
authToken,
maintenanceWindowId: maintenanceWindow._id,
});
updateCallback();
createToast({ body: "Maintenance window deleted successfully." });
} catch (error) {
createToast({ body: "Failed to delete maintenance window." });
logger.error("Failed to delete maintenance window", error);
} finally {
setIsLoading(false);
}
setIsOpen(false);
};
const handlePause = async () => {
try {
setIsLoading(true);
const data = {
active: !maintenanceWindow.active,
};
await networkService.editMaintenanceWindow({
authToken,
maintenanceWindowId: maintenanceWindow._id,
maintenanceWindow: data,
});
updateCallback();
} catch (error) {
logger.error(error);
createToast({ body: "Failed to pause maintenance window." });
} finally {
setIsLoading(false);
}
};
const handleEdit = () => {
navigate(`/maintenance/create/${maintenanceWindow._id}`);
};
const openMenu = (event) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": { p: theme.spacing(2.5) },
"& li": { m: 0 },
"& li:last-of-type": {
color: theme.palette.error.text,
},
},
},
}}
>
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
handleEdit();
}}
>
Edit
</MenuItem>
<MenuItem
onClick={(e) => {
handlePause();
closeMenu(e);
e.stopPropagation();
}}
>
{`${maintenanceWindow.active === true ? "Pause" : "Resume"}`}
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-maintenance" component="h2" variant="h2">
Do you really want to remove this maintenance window?
</Typography>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
Cancel
</Button>
<LoadingButton
loading={isLoading}
variant="contained"
color="error"
onClick={(e) => {
e.stopPropagation(e);
handleRemove(e);
}}
>
Delete
</LoadingButton>
</Stack>
</Stack>
</Modal>
</>
);
};
ActionsMenu.propTypes = {
maintenanceWindow: PropTypes.object,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,
};
export default ActionsMenu;
@@ -0,0 +1,363 @@
import PropTypes from "prop-types";
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Box,
TablePagination,
Stack,
Typography,
Button,
} from "@mui/material";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import ActionsMenu from "./ActionsMenu";
import { useState, useEffect, memo, useCallback, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
import { formatDurationRounded } from "../../../Utils/timeUtils";
import { StatusLabel } from "../../../Components/Label";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import dayjs from "dayjs";
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
const TablePaginationActions = (props) => {
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
const MaintenanceTable = ({
page,
setPage,
sort,
setSort,
maintenanceWindows,
maintenanceWindowCount,
updateCallback,
}) => {
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const theme = useTheme();
const dispatch = useDispatch();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const getTimeToNextWindow = (startTime, endTime, repeat) => {
//1. Advance time closest to next window as possible
const now = dayjs();
let start = dayjs(startTime);
let end = dayjs(endTime);
if (repeat > 0) {
// Advance time closest to next window as possible
while (start.isBefore(now) && end.isBefore(now)) {
start = start.add(repeat, "milliseconds");
end = end.add(repeat, "milliseconds");
}
}
//Check if we are in a window
if (now.isAfter(start) && now.isBefore(end)) {
return "In maintenance window";
}
if (start.isAfter(now)) {
const diffInMinutes = start.diff(now, "minutes");
const diffInHours = start.diff(now, "hours");
const diffInDays = start.diff(now, "days");
if (diffInMinutes < 60) {
return diffInMinutes + " minutes";
} else if (diffInHours < 24) {
return diffInHours + " hours";
} else if (diffInDays < 7) {
return diffInDays + " days";
} else {
return diffInDays + " days";
}
}
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "maintenance",
})
);
setPage(0);
};
/**
* Helper function to calculate the range of displayed rows.
* @returns {string}
*/
const getRange = () => {
let start = page * rowsPerPage + 1;
let end = Math.min(
page * rowsPerPage + rowsPerPage,
maintenanceWindowCount
);
return `${start} - ${end}`;
};
const handleSort = async (field) => {
let order = "";
if (sort.field !== field) {
order = "desc";
} else {
order = sort.order === "asc" ? "desc" : "asc";
}
setSort({ field, order });
};
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell
sx={{ cursor: "pointer" }}
onClick={() => handleSort("name")}
>
<Box>
Maintenance Window Name
<span
style={{
visibility: sort.field === "name" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
</TableCell>
<TableCell
sx={{ cursor: "pointer" }}
onClick={() => handleSort("status")}
>
{" "}
<Box width="max-content">
{" "}
Status
<span
style={{
visibility:
sort.field === "active" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
</TableCell>
<TableCell>Next Window</TableCell>
<TableCell>Repeat</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{maintenanceWindows.map((maintenanceWindow) => {
const text = maintenanceWindow.active ? "active" : "paused";
const status = maintenanceWindow.active ? "up" : "paused";
return (
<TableRow key={maintenanceWindow._id}>
<TableCell>{maintenanceWindow.name}</TableCell>
<TableCell>
<StatusLabel
status={status}
text={text}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
{getTimeToNextWindow(
maintenanceWindow.start,
maintenanceWindow.end,
maintenanceWindow.repeat
)}
</TableCell>
<TableCell>
{maintenanceWindow.repeat === 0
? "N/A"
: formatDurationRounded(maintenanceWindow.repeat)}
</TableCell>
<TableCell>
<ActionsMenu
maintenanceWindow={maintenanceWindow}
updateCallback={updateCallback}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
>
<Typography px={theme.spacing(2)} variant="body2" sx={{ opacity: 0.7 }}>
Showing {getRange()} of {maintenanceWindowCount} maintenance window(s)
</Typography>
<TablePagination
component="div"
count={maintenanceWindowCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: { mt: theme.spacing(-2) },
},
inputProps: { id: "pagination-dropdown" },
IconComponent: SelectorVertical,
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.background.main,
},
},
},
}}
sx={{
mt: theme.spacing(6),
color: theme.palette.text.secondary,
"& svg path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
</>
);
};
MaintenanceTable.propTypes = {
isAdmin: PropTypes.bool,
page: PropTypes.number,
setPage: PropTypes.func,
rowsPerPage: PropTypes.number,
setRowsPerPage: PropTypes.func,
sort: PropTypes.object,
setSort: PropTypes.func,
maintenanceWindows: PropTypes.array,
maintenanceWindowCount: PropTypes.number,
updateCallback: PropTypes.func,
};
const MemoizedMaintenanceTable = memo(MaintenanceTable);
export default MemoizedMaintenanceTable;
-36
View File
@@ -1,36 +0,0 @@
.maintenance-checklist-main {
max-width: 350px;
min-height: 390px;
margin: auto;
padding: 5px;
}
.maintenance-image {
width: 280px;
height: 150px;
background-color: whitesmoke;
border-radius: 10px;
}
.maintenance-title {
color: var(--secondary-color-light);
font-size: 16px;
font-weight: bold;
margin-top: 40px;
margin-bottom: 30px;
}
.checklist-item {
font-size: 13px;
color: var(--secondary-color-light);
display: flex;
margin-bottom: 10px;
}
.checklist-item-text {
margin-left: 10px;
}
.maintenance-checklist-main .maintenance-checklist-button {
margin-top: 30px;
}
+85 -11
View File
@@ -1,10 +1,48 @@
import { Box } from "@mui/material";
import { Box, Stack, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import Fallback from "../../Components/Fallback";
import { useState, useEffect } from "react";
import "./index.css";
import MaintenanceTable from "./MaintenanceTable";
import { useSelector } from "react-redux";
import { networkService } from "../../main";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { useNavigate } from "react-router-dom";
const Maintenance = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const { authToken } = useSelector((state) => state.auth);
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
const [page, setPage] = useState(0);
const [sort, setSort] = useState({});
const [updateTrigger, setUpdateTrigger] = useState(false);
const handleActionMenuDelete = () => {
setUpdateTrigger((prev) => !prev);
};
useEffect(() => {
const fetchMaintenanceWindows = async () => {
try {
const response = await networkService.getMaintenanceWindowsByTeamId({
authToken: authToken,
page: page,
rowsPerPage: rowsPerPage,
});
const { maintenanceWindows, maintenanceWindowCount } =
response.data.data;
setMaintenanceWindows(maintenanceWindows);
setMaintenanceWindowCount(maintenanceWindowCount);
} catch (error) {
console.log(error);
}
};
fetchMaintenanceWindows();
}, [authToken, page, rowsPerPage, updateTrigger]);
return (
<Box
@@ -21,16 +59,52 @@ const Maintenance = ({ isAdmin }) => {
},
}}
>
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={isAdmin}
/>
{maintenanceWindows.length > 0 && (
<Stack gap={theme.spacing(8)}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Breadcrumbs
list={[{ name: "maintenance", path: "/maintenance" }]}
/>
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/maintenance/create");
}}
sx={{ fontWeight: 500 }}
>
Create maintenance window
</Button>
</Stack>
<MaintenanceTable
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
sort={sort}
setSort={setSort}
maintenanceWindows={maintenanceWindows}
maintenanceWindowCount={maintenanceWindowCount}
updateCallback={handleActionMenuDelete}
/>
</Stack>
)}
{maintenanceWindows.length === 0 && (
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={true}
/>
)}
</Box>
);
};
@@ -1,34 +0,0 @@
.create-monitor p.MuiTypography-root,
.create-monitor button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
.create-monitor h6.MuiTypography-root,
.create-monitor .MuiBox-root .field + p.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
opacity: 0.8;
}
.create-monitor button.MuiButtonBase-root {
height: 34px;
}
.create-monitor .error-container {
position: relative;
}
.create-monitor .error-container p.MuiTypography-root.input-error {
opacity: 0.8;
position: absolute;
top: 0;
}
.create-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.create-monitor span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
font-size: var(--env-var-font-size-small);
height: 28px;
padding: 8px;
}
@@ -112,7 +112,7 @@ const CreateMonitor = () => {
{ [name]: value },
{ abortEarly: false }
);
console.log(error);
setErrors((prev) => {
const updatedErrors = { ...prev };
if (error) updatedErrors[name] = error.details[0].message;
+15 -14
View File
@@ -59,18 +59,19 @@ const Monitors = ({ isAdmin }) => {
mt={theme.spacing(5)}
>
<Greeting type="uptime" />
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
{isAdmin &&
monitorState?.monitorsSummary?.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
@@ -134,9 +135,9 @@ const Monitors = ({ isAdmin }) => {
</Box>
<Box width="25%" minWidth={150} ml="auto">
<Search
options={monitorState?.monitorsSummary.monitors}
options={monitorState?.monitorsSummary?.monitors ?? []}
filteredBy="name"
value={search}
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
@@ -244,7 +244,7 @@ const PageSpeedConfigure = () => {
},
}}
>
Editting...
Editing...
</Typography>
</Stack>
</Box>
+23 -20
View File
@@ -32,7 +32,7 @@ import PieChart from "./Charts/PieChart";
import useUtils from "../../Monitors/utils";
import "./index.css";
const PageSpeedDetails = () => {
const PageSpeedDetails = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
@@ -54,7 +54,7 @@ const PageSpeedDetails = () => {
normalize: null,
});
setMonitor(res?.data?.data ?? {});
setAudits(res?.data?.data?.checks?.[0]?.audits ?? []);
setAudits(res?.data?.data?.checks?.[0]?.audits ?? {});
} catch (error) {
logger.error(logger);
navigate("/not-found", { replace: true });
@@ -157,25 +157,27 @@ const PageSpeedDetails = () => {
</Typography>
</Stack>
</Box>
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.other.icon,
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.other.icon,
},
},
},
}}
>
<SettingsIcon />
Configure
</Button>
}}
>
<SettingsIcon />
Configure
</Button>
)}
</Stack>
<Stack direction="row" gap={theme.spacing(8)}>
<StatBox>
@@ -393,6 +395,7 @@ const PageSpeedDetails = () => {
};
PageSpeedDetails.propTypes = {
isAdmin: PropTypes.bool,
push: PropTypes.func,
};
+10 -8
View File
@@ -87,14 +87,16 @@ const PageSpeed = ({ isAdmin }) => {
mt={theme.spacing(5)}
>
<Greeting type="pagespeed" />
<Button
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
>
Create new
</Button>
{isAdmin && (
<Button
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Grid container spacing={theme.spacing(12)}>
+28 -4
View File
@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import { Box, Stack, Typography, Button } from "@mui/material";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
@@ -21,6 +21,7 @@ import { useState } from "react";
import { ConfigBox } from "./styled";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
import { useNavigate } from "react-router";
const SECONDS_PER_DAY = 86400;
@@ -33,10 +34,11 @@ const Settings = ({ isAdmin }) => {
const { timezone } = useSelector((state) => state.ui);
const [checksIsLoading, setChecksIsLoading] = useState(false);
const [form, setForm] = useState({
ttl: (checkTTL / SECONDS_PER_DAY).toString(),
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
});
const [errors, setErrors] = useState({});
const dispatch = useDispatch();
const navigate = useNavigate();
const handleChange = (event) => {
const { value, id } = event.target;
@@ -162,7 +164,7 @@ const Settings = ({ isAdmin }) => {
>
<ConfigBox>
<Box>
<Typography component="h1">General Settings</Typography>
<Typography component="h1">General settings</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
<Typography component="span">Display timezone</Typography>- The
timezone of the dashboard you publicly display.
@@ -216,7 +218,7 @@ const Settings = ({ isAdmin }) => {
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">Demo Monitors</Typography>
<Typography component="h1">Demo monitors</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Here you can add and remove demo monitors
</Typography>
@@ -249,6 +251,28 @@ const Settings = ({ isAdmin }) => {
</Stack>
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">Advanced settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Click here to modify advanced settings
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Box>
<Button
variant="contained"
onClick={() => {
navigate("/advanced-settings");
}}
>
Advanced Settings
</Button>
</Box>
</Stack>
</ConfigBox>
)}
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
+35 -9
View File
@@ -1,30 +1,56 @@
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL;
class Logger {
constructor(logLevel) {
const NO_OP = () => {};
import store from "../store";
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug";
const NO_OP = () => {};
class Logger {
constructor() {
let logLevel = LOG_LEVEL;
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
logLevel = state.settings.logLevel || "debug";
this.updateLogLevel(logLevel);
});
}
updateLogLevel(logLevel) {
if (logLevel === "none") {
this.info = NO_OP;
this.error = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.error = console.error.bind(console);
if (logLevel === "error") {
this.error = console.error.bind(console);
this.info = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.warn = console.warn.bind(console);
if (logLevel === "warn") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = NO_OP;
this.log = NO_OP;
return;
}
this.log = console.log.bind(console);
if (logLevel === "info") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = console.info.bind(console);
this.log = NO_OP;
return;
}
}
cleanup() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
export const logger = new Logger(LOG_LEVEL);
export const logger = new Logger();
+226 -6
View File
@@ -1,22 +1,51 @@
import axios from "axios";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
import { logger } from "./Logger";
class NetworkService {
constructor(store) {
constructor(store, dispatch, navigate) {
this.store = store;
this.axiosInstance = axios.create({ baseURL: BASE_URL });
this.dispatch = dispatch;
this.navigate = navigate;
let baseURL = BASE_URL;
this.axiosInstance = axios.create();
this.setBaseUrl(baseURL);
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
if (BASE_URL !== undefined) {
baseURL = BASE_URL;
} else if (state?.settings?.apiBaseUrl ?? null) {
baseURL = state.settings.apiBaseUrl;
} else {
baseURL = FALLBACK_BASE_URL;
}
this.setBaseUrl(baseURL);
});
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
logger.error(error);
if (error.response && error.response.status === 401) {
logger.error("Invalid token received");
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
navigate("/login");
}
return Promise.reject(error);
}
);
}
setBaseUrl = (url) => {
this.axiosInstance.defaults.baseURL = url;
};
cleanup() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
/**
*
* ************************************
@@ -101,7 +130,7 @@ class NetworkService {
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} config.teamId - The ID of the team whose monitors are to be retrieved.
* @param {number} [config.limit] - The maximum number of monitors to retrieve.
* @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none
* @param {Array<string>} [config.types] - The types of monitors to retrieve.
* @param {string} [config.status] - The status of the monitors to retrieve.
* @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors.
@@ -609,7 +638,7 @@ class NetworkService {
*/
async updateChecksTTL(config) {
return this.axiosInstance.put(
`/checks/ttl`,
`/checks/team/ttl`,
{ ttl: config.ttl },
{
headers: {
@@ -619,6 +648,197 @@ class NetworkService {
}
);
}
/**
* ************************************
* Get app settings
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*
*/
async getAppSettings(config) {
return this.axiosInstance.get("/settings", {
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
});
}
/**
*
* ************************************
* Create a new monitor
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.settings - The monitor object to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async updateAppSettings(config) {
return this.axiosInstance.put(`/settings`, config.settings, {
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
});
}
/**
* ************************************
* Creates a maintenance window
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async createMaintenanceWindow(config) {
return this.axiosInstance.post(
`/maintenance-window`,
config.maintenanceWindow,
{
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Edits a maintenance window
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.maintenanceWindowId - The maintenance window id.
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async editMaintenanceWindow(config) {
return this.axiosInstance.put(
`/maintenance-window/${config.maintenanceWindowId}`,
config.maintenanceWindow,
{
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Get maintenance window by id
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async getMaintenanceWindowById(config) {
const { authToken, maintenanceWindowId } = config;
return this.axiosInstance.get(
`/maintenance-window/${maintenanceWindowId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Get maintenance windows by teamId
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.active] - The status of the maintenance windows to retrieve.
* @param {number} [config.page] - The page number for pagination.
* @param {number} [config.rowsPerPage] - The number of rows per page for pagination.
* @param {string} [config.field] - The field to sort by.
* @param {string} [config.order] - The order in which to sort the field.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async getMaintenanceWindowsByTeamId(config) {
const { authToken, active, page, rowsPerPage, field, order } = config;
const params = new URLSearchParams();
if (active) params.append("status", active);
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (field) params.append("field", field);
if (order) params.append("order", order);
return this.axiosInstance.get(
`/maintenance-window/team?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Delete maintenance window by id
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async deleteMaintenanceWindow(config) {
const { authToken, maintenanceWindowId } = config;
return this.axiosInstance.delete(
`/maintenance-window/${maintenanceWindowId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
}
export default NetworkService;
let networkService;
export const setNetworkService = (service) => {
networkService = service;
};
export { networkService };
@@ -0,0 +1,15 @@
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { setNetworkService } from "./NetworkService";
import NetworkService from "./NetworkService";
import { store } from "../store";
const NetworkServiceProvider = ({ children }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const networkService = new NetworkService(store, dispatch, navigate);
setNetworkService(networkService);
return children;
};
export default NetworkServiceProvider;
+6
View File
@@ -4,6 +4,12 @@ import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
export const MS_PER_DAY = 24 * MS_PER_HOUR;
export const MS_PER_WEEK = MS_PER_DAY * 7;
export const formatDuration = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
+20 -35
View File
@@ -1,4 +1,5 @@
import joi from "joi";
import dayjs from "dayjs";
const nameSchema = joi
.string()
@@ -77,6 +78,7 @@ const credentials = joi.object({
}),
role: joi.array(),
teamId: joi.string().allow("").optional(),
inviteToken: joi.string().allow(""),
});
const monitorValidation = joi.object({
@@ -112,50 +114,33 @@ const imageValidation = joi.object({
}),
});
const maintenanceWindowValidation = joi.object({
repeat: joi.number().valid(1, 2, 3).required().messages({
"number.base": "Repeat must be a number.",
"any.only": "Repeat must be one of [1, 2, 3].",
"any.required": "Repeat is required.",
}),
date: joi.date().required().messages({
"date.base": "Date must be a valid date.",
"any.required": "Date is required.",
}),
startTime: joi.string().required().messages({
"string.base": "Start time must be a valid time.",
"any.required": "Start time is required.",
}),
duration: joi.number().required().messages({
"number.empty": "duration is required.",
"number.base": "Duration must be a number.",
"any.required": "Duration is required.",
}),
unit: joi.string().valid("minutes", "hours", "days").required().messages({
"string.base": "Unit must be a string.",
"any.only": "Unit must be one of ['minutes', 'hours', 'days'].",
"any.required": "Unit is required.",
}),
displayName: joi.string().max(50).required().messages({
"string.empty": "Display name is required.",
"string.max": "Display name must be less than 50 characters long",
}),
addMonitors: joi.string().max(50).required().messages({
"string.empty": "Add monitors is required.",
"string.max": "Add monitors must be less than 50 characters long",
}),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
}),
});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
return helpers.error("any.invalid");
}
return value;
};
const maintenanceWindowValidation = joi.object({
repeat: joi.string(),
startDate: joi.custom(dayjsValidator, "Day.js date validation"),
startTime: joi.custom(dayjsValidator, "Day.js date validation"),
duration: joi.number(),
durationUnit: joi.string(),
name: joi.string(),
monitors: joi.array().min(1),
});
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
settingsValidation,
maintenanceWindowValidation,
};
+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 8.33342H1.5M12.3333 1.66675V5.00008M5.66667 1.66675V5.00008M5.5 18.3334H12.5C13.9001 18.3334 14.6002 18.3334 15.135 18.0609C15.6054 17.8212 15.9878 17.4388 16.2275 16.9684C16.5 16.4336 16.5 15.7335 16.5 14.3334V7.33342C16.5 5.93328 16.5 5.23322 16.2275 4.69844C15.9878 4.22803 15.6054 3.84558 15.135 3.6059C14.6002 3.33341 13.9001 3.33341 12.5 3.33341H5.5C4.09987 3.33341 3.3998 3.33341 2.86502 3.6059C2.39462 3.84558 2.01217 4.22803 1.77248 4.69844C1.5 5.23322 1.5 5.93328 1.5 7.33341V14.3334C1.5 15.7335 1.5 16.4336 1.77248 16.9684C2.01217 17.4388 2.39462 17.8212 2.86502 18.0609C3.3998 18.3334 4.09987 18.3334 5.5 18.3334Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 830 B

+8 -5
View File
@@ -1,18 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { BrowserRouter as Router, HashRouter } from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import NetworkService from "./Utils/NetworkService.js";
export const networkService = new NetworkService(store);
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
import { networkService } from "./Utils/NetworkService";
export { networkService };
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Router>
<App />
<NetworkServiceProvider>
<App />
</NetworkServiceProvider>
</Router>
</PersistGate>
</Provider>
+4 -1
View File
@@ -4,6 +4,7 @@ import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import authReducer from "./Features/Auth/authSlice";
import uiReducer from "./Features/UI/uiSlice";
import settingsReducer from "./Features/Settings/settingsSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore, createTransform } from "redux-persist";
@@ -21,7 +22,7 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
whitielist: ["auth", "monitors", "pageSpeed", "ui"],
whitelist: ["auth", "monitors", "pageSpeed", "ui", "settings"],
transforms: [authTransform],
};
@@ -30,6 +31,7 @@ const rootReducer = combineReducers({
auth: authReducer,
pageSpeedMonitors: pageSpeedMonitorReducer,
ui: uiReducer,
settings: settingsReducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
@@ -49,3 +51,4 @@ export const store = configureStore({
});
export const persistor = persistStore(store);
export default store;
+7 -3
View File
@@ -1,7 +1,11 @@
*.sh
!quickstart.sh
!build_images.sh
mongo/data/*
redis/data/*
dev/mongo/data/*
dev/redis/data/*
dist/mongo/data/*
dist/redis/data/*
prod/mongo/data/*
prod/redis/data/*
*.env
certbot/*
prod/certbot/*
-22
View File
@@ -1,22 +0,0 @@
FROM node:20-alpine as build
WORKDIR /app
COPY ./Client/package*.json ./
RUN npm install
COPY ./Client .
RUN npm run build
# RUN npm install -g serve
# CMD ["serve","-s", "dist", "-l", "5173"]
FROM nginx:1.27.1-alpine
# COPY ./Docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
@@ -2,19 +2,19 @@
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
cd ..
cd ../..
#Client
client="./Docker/client.Dockerfile"
client="./Docker/dev/client.Dockerfile"
# MongoDB
mongoDB="./Docker/mongoDB.Dockerfile"
mongoDB="./Docker/dev/mongoDB.Dockerfile"
# Redis
redis="./Docker/redis.Dockerfile"
redis="./Docker/dev/redis.Dockerfile"
# Server
server="./Docker/server.Dockerfile"
server="./Docker/dev/server.Dockerfile"
docker build -f $client -t uptime_client .
docker build -f $mongoDB -t uptime_database_mongo .
+15
View File
@@ -0,0 +1,15 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY ../../Client/package*.json ./
RUN npm install
COPY ../../Client .
RUN npm run build
RUN npm install -g serve
CMD ["serve","-s", "dist", "-l", "5173"]
+30
View File
@@ -0,0 +1,30 @@
services:
client:
image: uptime_client:latest
ports:
- "5173:5173"
depends_on:
- server
server:
image: uptime_server:latest
ports:
- "5000:5000"
env_file:
- server.env
depends_on:
- redis
- mongodb
redis:
image: uptime_redis:latest
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
command: ["mongod", "--quiet"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
+3
View File
@@ -0,0 +1,3 @@
FROM mongo
EXPOSE 27017
CMD ["mongod"]
File renamed without changes.
+13
View File
@@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /app
COPY ../../Server/package*.json ./
RUN npm install
COPY ../../Server/ ./
EXPOSE 5000
CMD ["node", "index.js"]
Vendored Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
cd ../..
#Client
client="./Docker/dist/client.Dockerfile"
# MongoDB
mongoDB="./Docker/dist/mongoDB.Dockerfile"
# Redis
redis="./Docker/dist/redis.Dockerfile"
# Server
server="./Docker/dist/server.Dockerfile"
docker build -f $client -t dist_uptime_client .
docker build -f $mongoDB -t dist_uptime_database_mongo .
docker build -f $redis -t dist_uptime_redis .
docker build -f $server -t dist_uptime_server .
echo "All images built"
+19
View File
@@ -0,0 +1,19 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY ../../Client/package*.json ./
RUN npm install
COPY ../../Client .
RUN npm run build
FROM nginx:1.27.1-alpine
COPY ./Docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
@@ -6,17 +6,16 @@ services:
- "443:443"
depends_on:
- server
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
server:
image: bluewaveuptime/uptime_server:latest
ports:
- "5000:5000"
env_file:
- server.env
depends_on:
- redis
- mongodb
environment:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
- REDIS_HOST=redis
redis:
image: bluewaveuptime/uptime_redis:latest
ports:
@@ -25,10 +24,8 @@ services:
- ./redis/data:/data
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
command: ["mongod", "--quiet", "--auth"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
env_file:
- mongo.env
command: ["mongod", "--quiet"]
ports:
- "27017:27017"
+3
View File
@@ -0,0 +1,3 @@
FROM mongo
EXPOSE 27017
CMD ["mongod"]
+35
View File
@@ -0,0 +1,35 @@
server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+2
View File
@@ -0,0 +1,2 @@
FROM redis
EXPOSE 6379
+13
View File
@@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /app
COPY ../../Server/package*.json ./
RUN npm install
COPY ../../Server/ ./
EXPOSE 5000
CMD ["node", "index.js"]
-4
View File
@@ -1,4 +0,0 @@
FROM mongo
COPY ./Docker/mongo/init/create_users.js /docker-entrypoint-initdb.d/
EXPOSE 27017
CMD ["mongod"]
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Change directory to root directory for correct Docker Context
cd ../..
#Client
client="./Docker/prod/client.Dockerfile"
# MongoDB
mongoDB="./Docker/prod/mongoDB.Dockerfile"
# Redis
redis="./Docker/prod/redis.Dockerfile"
# Server
server="./Docker/prod/server.Dockerfile"
docker build -f $client -t uptime_client .
docker build -f $mongoDB -t uptime_database_mongo .
docker build -f $redis -t uptime_redis .
docker build -f $server -t uptime_server .
echo "All images built"
+17
View File
@@ -0,0 +1,17 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY ../../Client/package*.json ./
RUN npm install
COPY ../../Client .
RUN npm run build
FROM nginx:1.27.1-alpine
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
@@ -40,4 +40,4 @@ services:
- ./mongo/data:/data/db
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
env_file:
- mongo.env
- mongo.env
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
FROM mongo
COPY ./Docker/prod/mongo/init/create_users.js /docker-entrypoint-initdb.d/
EXPOSE 27017
CMD ["mongod"]
@@ -23,6 +23,15 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
@@ -48,4 +57,13 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+2
View File
@@ -0,0 +1,2 @@
FROM redis
EXPOSE 6379
File renamed without changes.
-123
View File
@@ -1,123 +0,0 @@
default_server_base_url="http://localhost:5000/api/v1"
default_client_host="http://localhost"
default_jwt_secret="my_secret"
default_db_type="MongoDB"
default_redis_host="redis"
default_redis_port=6379
default_token_ttl="99d"
default_db_username="uptime_user"
default_db_password="uptime_password"
default_system_email_host="smtp.gmail.com"
default_system_email_port=465
echo "Welcome to the Uptime Monitor Setup Script! \n"
echo
echo "Configuring server"
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' '*'
echo
db_type=$default_db_type
redis_host=$default_redis_host
redis_port=$default_redis_port
jwt_secret=$default_jwt_secret
read -p "Enter a username for your database [$default_db_username]: " db_username
db_username="${db_username:-$default_db_username}"
read -p "Enter a password for your database [$default_db_password]: " db_password
db_password="${db_password:-$default_db_password}"
read -p "Enter your system email host [$default_system_email_host]: " system_email_host
system_email_host="${system_email_host:-$default_system_email_host}"
echo "System email host: $system_email_host"
echo
read -p "Enter your DB connection string [$default_db_connection_string]: " db_connection_string
db_connection_string="mongodb://${db_username}:${db_password}@mongodb:27017/uptime_db"
echo "DB connection string: $db_connection_string"
echo
read -p "Enter your system email port [$default_system_email_port]: " system_email_port
system_email_port="${system_email_port:-$default_system_email_port}"
echo "System email port: $system_email_port"
echo
read -p "Enter your system email address: " system_email_address
echo "System email address: $system_email_address"
echo
read -p "Enter your system email password: " system_email_password
echo "System email password: $system_email_password"
echo
read -p "Enter your Token TTL [$default_token_ttl]: " token_ttl
token_ttl="${token_ttl:-$default_token_ttl}"
echo "Token TTL: $token_ttl"
echo
read -p "Enter your Pagespeed API key: " pagespeed_api_key
echo "Pagespeed API key: $pagespeed_api_key"
echo
echo "Writing to ./server.env"
echo
{
echo "CLIENT_HOST=\"$client_host\""
echo "JWT_SECRET=\"$jwt_secret\""
echo "DB_TYPE=\"$db_type\""
echo "DB_CONNECTION_STRING=\"$db_connection_string\""
echo "REDIS_HOST=\"$redis_host\""
echo "REDIS_PORT=$redis_port"
echo "SYSTEM_EMAIL_HOST=\"$system_email_host\""
echo "SYSTEM_EMAIL_PORT=$system_email_port"
echo "SYSTEM_EMAIL_ADDRESS=\"$system_email_address\""
echo "SYSTEM_EMAIL_PASSWORD=\"$system_email_password\""
echo "TOKEN_TTL=\"$token_ttl\""
echo "PAGESPEED_API_KEY=\"$pagespeed_api_key\""
} > ./server.env
echo
{
echo USERNAME_ENV_VAR=${db_username}
echo PASSWORD_ENV_VAR=${db_password}
} > ./mongo.env
mkdir -p ./nginx/conf.d/
cat <<EOL > ./nginx/conf.d/default.conf
server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOL
+13 -2108
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -2,3 +2,5 @@ node_modules
.env
*.log
*.sh
.nyc_output
coverage
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
require: ["chai/register-expect.js"], // Include Chai's "expect" interface globally
spec: "tests/**/*.test.js", // Specify test files
timeout: 5000, // Set test-case timeout in milliseconds
recursive: true, // Include subdirectories
reporter: "spec", // Use the "spec" reporter
exit: true, // Force Mocha to quit after tests complete
};
+8
View File
@@ -0,0 +1,8 @@
{
"all": true,
"include": ["**/*.js"],
"exclude": ["**/*.test.js"],
"reporter": ["html", "text", "lcov"],
"sourceMap": false,
"instrument": true
}
+1 -1
View File
@@ -1,4 +1,4 @@
const PORT = process.env.PORT || 5000;
const PORT = 5000;
const connectDbAndRunServer = async (app, db) => {
try {
-192
View File
@@ -1,192 +0,0 @@
const logger = require("../utils/logger");
require("dotenv").config();
const {
createAlertParamValidation,
createAlertBodyValidation,
getAlertsByUserIdParamValidation,
getAlertsByMonitorIdParamValidation,
getAlertByIdParamValidation,
editAlertParamValidation,
editAlertBodyValidation,
deleteAlertParamValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "alertController";
/**
* Creates an alert for a monitor
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const createAlert = async (req, res, next) => {
try {
await createAlertParamValidation.validateAsync(req.params);
await createAlertBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const alertData = { ...req.body };
const alert = await req.db.createAlert(alertData);
return res
.status(200)
.json({ success: true, msg: successMessages.ALERT_CREATE, data: alert });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createAlert") : null;
next(error);
}
};
/**
* Gets all alerts for a user
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getAlertsByUserId = async (req, res, next) => {
try {
await getAlertsByUserIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const alerts = await req.db.getAlertsByUserId(req.params.userId);
return res.status(200).json({
success: true,
msg: successMessages.ALERT_GET_BY_USER,
data: alerts,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
const getAlertsByMonitorId = async (req, res, next) => {
try {
await getAlertsByMonitorIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const alerts = await req.db.getAlertsByMonitorId(req.params.monitorId);
return res.status(200).json({
success: true,
msg: successMessages.ALERT_GET_BY_MONITOR,
data: alerts,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
const getAlertById = async (req, res, next) => {
try {
await getAlertByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const alert = await req.db.getAlertById(req.params.alertId);
return res.status(200).json({
success: true,
msg: successMessages.ALERT_GET_BY_ID,
data: alert,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
const editAlert = async (req, res, next) => {
try {
await editAlertParamValidation.validateAsync(req.params);
await editAlertBodyValidation.validateAsync(req.body);
} catch (error) {
console.log(error);
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const alertData = { ...req.body };
const alert = await req.db.editAlert(req.params.alertId, alertData);
return res.status(200).json({
success: true,
msg: successMessages.ALERT_EDIT,
data: alert,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
const deleteAlert = async (req, res, next) => {
try {
await deleteAlertParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
error.service = SERVICE_NAME;
next(error);
return;
}
try {
const deleted = await req.db.deleteAlert(req.params.alertId);
return res.status(200).json({
success: true,
msg: successMessages.ALERT_DELETE,
data: deleted,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
module.exports = {
createAlert,
getAlertsByUserId,
getAlertsByMonitorId,
getAlertById,
editAlert,
deleteAlert,
};
+168 -258
View File
@@ -1,4 +1,3 @@
const express = require("express");
const {
registrationBodyValidation,
loginValidation,
@@ -7,67 +6,67 @@ const {
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
deleteUserParamValidation,
inviteRoleValidation,
inviteBodyValidation,
inviteVerifciationBodyValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
const { errorMessages, successMessages } = require("../utils/messages");
var jwt = require("jsonwebtoken");
const SERVICE_NAME = "auth";
const jwt = require("jsonwebtoken");
const SERVICE_NAME = "AuthController";
const { getTokenFromHeaders } = require("../utils/utils");
const crypto = require("crypto");
const { handleValidationError, handleError } = require("./controllerUtils");
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
const issueToken = (payload) => {
//TODO Add proper expiration date
const tokenTTL = process.env.TOKEN_TTL ? process.env.TOKEN_TTL : "2h";
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: tokenTTL });
const issueToken = (payload, appSettings) => {
try {
const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h";
return jwt.sign(payload, appSettings.jwtSecret, { expiresIn: tokenTTL });
} catch (error) {
throw handleError(error, SERVICE_NAME, "issueToken");
}
};
/**
* @function
* @param {express.Request} req
* @param {express.Response} res
* @returns {{success: Boolean, msg: String}}
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.inviteToken - The invite token for registration.
* @property {Object} req.file - The file object for the user's profile image.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const registerController = async (req, res, next) => {
const registerUser = async (req, res, next) => {
// joi validation
try {
await registrationBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// TODO Can there be more than one admin?
// // Check if an admin user exists, if so, error
// try {
// const admin = await req.db.checkAdmin(req, res);
// console.log(admin);
// if (admin === true) {
// throw new Error(errorMessages.AUTH_ADMIN_EXISTS);
// }
// } catch (error) {
// console.log("WEEEEEEE", error.message);
// error.service = SERVICE_NAME;
// error.status = 403;
// next(error);
// return;
// }
// Create a new user
try {
const { inviteToken } = req.body;
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await req.db.checkSuperadmin(req, res);
if (superAdminExists) {
await req.db.getInviteTokenAndDelete(inviteToken);
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await req.db.updateAppSettings({ jwtSecret });
}
const newUser = await req.db.insertUser({ ...req.body }, req.file);
logger.info(successMessages.AUTH_CREATE_USER, {
service: SERVICE_NAME,
@@ -78,8 +77,8 @@ const registerController = async (req, res, next) => {
delete userForToken.profileImage;
delete userForToken.avatarImage;
const token = issueToken(userForToken);
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userForToken, appSettings);
req.emailService
.buildAndSendEmail(
"welcomeEmailTemplate",
@@ -100,28 +99,28 @@ const registerController = async (req, res, next) => {
data: { user: newUser, token: token },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "registerController") : null;
next(error);
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
/**
* Returns JWT with User info
* Logs in a user by validating the user's credentials and issuing a JWT token.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user.
* @property {string} req.body.password - The password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
*/
const loginController = async (req, res, next) => {
const loginUser = async (req, res, next) => {
try {
await loginValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
@@ -132,7 +131,8 @@ const loginController = async (req, res, next) => {
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD));
return;
}
// Remove password from user object. Should this be abstracted to DB layer?
@@ -141,8 +141,8 @@ const loginController = async (req, res, next) => {
delete userWithoutPassword.avatarImage;
// Happy path, return token
const token = issueToken(userWithoutPassword);
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
@@ -152,23 +152,31 @@ const loginController = async (req, res, next) => {
data: { user: userWithoutPassword, token: token },
});
} catch (error) {
error.status = 500;
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "loginController") : null;
next(error);
next(handleError(error, SERVICE_NAME, "loginController"));
}
};
const userEditController = async (req, res, next) => {
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.userId - The ID of the user to be edited.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.password - The current password of the user.
* @property {string} req.body.newPassword - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
*/
const editUser = async (req, res, next) => {
try {
await editUserParamValidation.validateAsync(req.params);
await editUserBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
@@ -184,14 +192,11 @@ const userEditController = async (req, res, next) => {
try {
// Change Password check
if (req.body.password && req.body.newPassword) {
const newPassword = req.body.newPassword;
const password = req.body.password;
// Compare password
// Get token from headers
const token = getTokenFromHeaders(req.headers);
// Get email from token
const { email } = jwt.verify(token, process.env.JWT_SECRET);
const { jwtSecret } = req.settingsService.getSettings();
const { email } = jwt.verify(token, jwtSecret);
// Add user email to body for DB operation
req.body.email = email;
// Get user
@@ -202,7 +207,8 @@ const userEditController = async (req, res, next) => {
if (!match) {
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
error.status = 403;
throw error;
next(error);
return;
}
// If a match, update the password
req.body.password = req.body.newPassword;
@@ -215,96 +221,20 @@ const userEditController = async (req, res, next) => {
data: updatedUser,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "userEditController") : null;
next(error);
return;
}
};
const inviteController = async (req, res, next) => {
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
const { role, firstname, teamId } = jwt.decode(token);
req.body.teamId = teamId;
try {
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
const inviteToken = await req.db.requestInviteToken(req, res);
req.emailService
.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
},
req.body.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending invite email", {
service: SERVICE_NAME,
error: error.message,
});
});
return res
.status(200)
.json({ success: true, msg: "Invite sent", data: inviteToken });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "inviteController") : null;
next(error);
return;
}
};
const inviteVerifyController = async (req, res, next) => {
try {
await inviteVerifciationBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const invite = await req.db.getInviteToken(req, res);
res
.status(200)
.json({ status: "success", msg: "Invite verified", data: invite });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "inviteVerifyController")
: null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "userEditController"));
}
};
/**
* Checks to see if an admin account exists
* Checks if a superadmin account exists in the database.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @param {Object} req - The Express request object.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin.
* @throws {Error} If there is an error during the process.
*/
const checkSuperadminController = async (req, res) => {
const checkSuperAdminExists = async (req, res, next) => {
try {
const superAdminExists = await req.db.checkSuperadmin(req, res);
return res.status(200).json({
@@ -313,33 +243,27 @@ const checkSuperadminController = async (req, res) => {
data: superAdminExists,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "checkSuperadminController")
: null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
}
};
/**
* Returns a recovery token
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
* @async
* @param {Express.Request} req
* @property {Object} req.body
* @property {string} req.body.email
* @property {EmailService} req.body.emailService
* @returns {Promise<Express.Response>}
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user requesting recovery.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const recoveryRequestController = async (req, res, next) => {
const requestRecovery = async (req, res, next) => {
try {
await recoveryValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
@@ -350,13 +274,18 @@ const recoveryRequestController = async (req, res, next) => {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const email = req.body.email;
const url = `${process.env.CLIENT_HOST}/set-new-password/${recoveryToken.token}`;
const { clientHost } = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail(
"passwordResetTemplate",
{ name, email, url },
{
name,
email,
url,
},
email,
"Bluewaves Uptime Password Resest"
"Bluewave Uptime Password Reset"
);
return res.status(200).json({
@@ -366,88 +295,73 @@ const recoveryRequestController = async (req, res, next) => {
});
}
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "recoveryRequestController")
: null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
}
};
/**
* Returns a recovery token
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* @async
* @param {Express.Request} req
* @property {Object} req.body
* @property {string} req.body.token
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token to be validated.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const validateRecoveryTokenController = async (req, res, next) => {
const validateRecovery = async (req, res, next) => {
try {
await recoveryTokenValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
await req.db.validateRecoveryToken(req, res);
// TODO Redirect user to reset password after validating token
return res.status(200).json({
success: true,
msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "validateRecoveryTokenController")
: null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
}
};
/**
* Returns an updated user
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
* @async
* @param {Express.Request} req
* @property {Object} req.body
* @property {string} req.body.token
* @property {string} req.body.password
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token.
* @property {string} req.body.password - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const resetPasswordController = async (req, res, next) => {
const resetPassword = async (req, res, next) => {
try {
await newPasswordValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const user = await req.db.resetPassword(req, res);
const token = issueToken(user._doc);
const appSettings = await req.settingsService.getSettings();
const token = issueToken(user._doc, appSettings);
res.status(200).json({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: { user, token },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "resetPasswordController")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
}
};
@@ -460,16 +374,17 @@ const resetPasswordController = async (req, res, next) => {
* @returns {Object} The response object with success status and message.
* @throws {Error} If user validation fails or user is not found in the database.
*/
const deleteUserController = async (req, res, next) => {
const deleteUser = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const decodedToken = jwt.decode(token);
const { _id, email } = decodedToken;
const { email } = decodedToken;
// Check if the user exists
const user = await req.db.getUserByEmail(email);
if (!user) {
throw new Error(errorMessages.DB_USER_NOT_FOUND);
next(new Error(errorMessages.DB_USER_NOT_FOUND));
return;
}
// 1. Find all the monitors associated with the team ID if superadmin
@@ -477,61 +392,56 @@ const deleteUserController = async (req, res, next) => {
const result = await req.db.getMonitorsByTeamId({
params: { teamId: user.teamId },
});
if (user.role.includes("superadmin") && result?.monitors.length > 0) {
if (user.role.includes("superadmin")) {
//2. Remove all jobs, delete checks and alerts
await Promise.all(
monitors.map(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
result?.monitors.length > 0 &&
(await Promise.all(
result.monitors.map(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
})
));
await req.db.deleteChecks(monitor._id);
await req.db.deleteAlertByMonitorId(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
})
);
// 3. Delete each monitor
// 3. Delete team
await req.db.deleteTeam(user.teamId);
// 4. Delete all other team members
await req.db.deleteAllOtherUsers();
// 5. Delete each monitor
await req.db.deleteMonitorsByUserId(user._id);
}
// 4. Delete the user by id
// 6. Delete the user by id
await req.db.deleteUser(user._id);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_DELETE_USER,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteUserController") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteUserController"));
}
};
const getAllUsersController = async (req, res) => {
const getAllUsers = async (req, res) => {
try {
const allUsers = await req.db.getAllUsers(req, res);
res
.status(200)
.json({ success: true, msg: "Got all users", data: allUsers });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getAllUsersController")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
}
};
module.exports = {
registerController,
loginController,
userEditController,
inviteController,
inviteVerifyController,
checkSuperadminController,
recoveryRequestController,
validateRecoveryTokenController,
resetPasswordController,
deleteUserController,
getAllUsersController,
registerUser,
loginUser,
editUser,
checkSuperadminExists: checkSuperAdminExists,
requestRecovery,
validateRecovery,
resetPassword,
deleteUser,
getAllUsers,
};
+15 -49
View File
@@ -13,17 +13,14 @@ const { successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const SERVICE_NAME = "checkController";
const { handleValidationError, handleError } = require("./controllerUtils");
const createCheck = async (req, res, next) => {
try {
await createCheckParamValidation.validateAsync(req.params);
await createCheckBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -34,9 +31,7 @@ const createCheck = async (req, res, next) => {
.status(200)
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
} catch (error) {
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createCheck") : null;
next(error);
next(handleError(error, SERVICE_NAME, "createCheck"));
}
};
@@ -45,11 +40,7 @@ const getChecks = async (req, res, next) => {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -62,9 +53,7 @@ const getChecks = async (req, res, next) => {
data: { checksCount, checks },
});
} catch (error) {
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
@@ -73,11 +62,7 @@ const getTeamChecks = async (req, res, next) => {
await getTeamChecksParamValidation.validateAsync(req.params);
await getTeamChecksQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -88,9 +73,7 @@ const getTeamChecks = async (req, res, next) => {
data: checkData,
});
} catch (error) {
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getTeamChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
}
};
@@ -98,11 +81,7 @@ const deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -114,9 +93,7 @@ const deleteChecks = async (req, res, next) => {
data: { deletedCount },
});
} catch (error) {
error.service === undefined ? (error.serivce = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteChecks"));
}
};
@@ -124,10 +101,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
try {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecksByTeam") : null;
error.status = 422;
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -139,9 +113,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
data: { deletedCount },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecksByTeamId") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
}
};
@@ -151,19 +123,15 @@ const updateChecksTTL = async (req, res, next) => {
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "updateChecksTTL") : null;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
// Get user's teamId
const token = getTokenFromHeaders(req.headers);
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await req.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
@@ -171,9 +139,7 @@ const updateChecksTTL = async (req, res, next) => {
msg: successMessages.CHECK_UPDATE_TTL,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "updateTTL") : null;
next(error);
next(handleError(error, SERVICE_NAME, "updateTTL"));
}
};
+19
View File
@@ -0,0 +1,19 @@
const handleValidationError = (error, serviceName) => {
error.status = 422;
error.service = serviceName;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
return error;
};
const handleError = (error, serviceName, method, code = 500) => {
error.code === undefined ? (error.code = code) : null;
error.service === undefined ? (error.service = serviceName) : null;
error.method === undefined ? (error.method = method) : null;
return error;
};
module.exports = {
handleValidationError,
handleError,
};
+25 -27
View File
@@ -1,12 +1,12 @@
const {
inviteRoleValidation,
inviteBodyValidation,
inviteVerifciationBodyValidation,
inviteVerificationBodyValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
const { errorMessages, successMessages } = require("../utils/messages");
var jwt = require("jsonwebtoken");
const jwt = require("jsonwebtoken");
const { handleError, handleValidationError } = require("./controllerUtils");
const SERVICE_NAME = "inviteController";
const getTokenFromHeaders = (headers) => {
@@ -20,7 +20,20 @@ const getTokenFromHeaders = (headers) => {
return parts[1];
};
const inviteController = async (req, res, next) => {
/**
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user to be invited.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const issueInvitation = async (req, res, next) => {
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
@@ -30,21 +43,18 @@ const inviteController = async (req, res, next) => {
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
const inviteToken = await req.db.requestInviteToken({ ...req.body });
const { clientHost } = req.settingsService.getSettings();
req.emailService
.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
link: `${clientHost}/register/${inviteToken.token}`,
},
req.body.email,
"Welcome to Uptime Monitor"
@@ -60,22 +70,15 @@ const inviteController = async (req, res, next) => {
.status(200)
.json({ success: true, msg: "Invite sent", data: inviteToken });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "inviteController") : null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "inviteController"));
}
};
const inviteVerifyController = async (req, res, next) => {
try {
await inviteVerifciationBodyValidation.validateAsync(req.body);
await inviteVerificationBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -85,16 +88,11 @@ const inviteVerifyController = async (req, res, next) => {
.status(200)
.json({ status: "success", msg: "Invite verified", data: invite });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "inviteVerifyController")
: null;
next(error);
return;
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
}
};
module.exports = {
inviteController,
inviteController: issueInvitation,
inviteVerifyController,
};
+113 -61
View File
@@ -1,81 +1,96 @@
const {
createMaintenanceWindowParamValidation,
createMaintenanceWindowBodyValidation,
getMaintenanceWindowsByUserIdParamValidation,
editMaintenanceWindowByIdParamValidation,
editMaintenanceByIdWindowBodyValidation,
getMaintenanceWindowByIdParamValidation,
getMaintenanceWindowsByMonitorIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} = require("../validation/joi");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const { successMessages } = require("../utils/messages");
const { handleValidationError, handleError } = require("./controllerUtils");
const SERVICE_NAME = "maintenanceWindowController";
const { errorMessages, successMessages } = require("../utils/messages");
const createMaintenanceWindow = async (req, res, next) => {
const createMaintenanceWindows = async (req, res, next) => {
try {
await createMaintenanceWindowParamValidation.validateAsync(req.params);
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const data = {
monitorId: req.params.monitorId,
...req.body,
};
if (data.oneTime === true) {
data.expiry = data.end;
}
const maintenanceWindow = await req.db.createMaintenanceWindow(data);
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return req.db.createMaintenanceWindow({
teamId,
monitorId,
name: req.body.name,
active: req.body.active ? req.body.active : true,
repeat: req.body.repeat,
start: req.body.start,
end: req.body.end,
});
});
await Promise.all(dbTransactions);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
data: maintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "createMaintenanceWindow")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
}
};
const getMaintenanceWindowsByUserId = async (req, res, next) => {
const getMaintenanceWindowById = async (req, res, next) => {
try {
await getMaintenanceWindowsByUserIdParamValidation.validateAsync(
req.params
);
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindows = await req.db.getMaintenanceWindowsByUserId(
req.params.userId
const maintenanceWindow = await req.db.getMaintenanceWindowById(
req.params.id
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
data: maintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
}
};
const getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const maintenanceWindows = await req.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
);
return res.status(201).json({
success: true,
msg: successMessages.MAINTEANCE_WINDOW_GET_BY_USER,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: maintenanceWindows,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowsByUserId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
}
};
@@ -85,11 +100,7 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
req.params
);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -100,19 +111,60 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
return res.status(201).json({
success: true,
msg: successMessages.MAINTEANCE_WINDOW_GET_BY_USER,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
data: maintenanceWindows,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowsByMonitorId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
}
};
module.exports = {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowsByMonitorId,
const deleteMaintenanceWindow = async (req, res, next) => {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await req.db.deleteMaintenanceWindowById(req.params.id);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
}
};
const editMaintenanceWindow = async (req, res, next) => {
try {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const editedMaintenanceWindow = await req.db.editMaintenanceWindowById(
req.params.id,
req.body
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
data: editedMaintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
}
};
module.exports = {
createMaintenanceWindows,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindow,
editMaintenanceWindow,
};
+138 -186
View File
@@ -11,8 +11,6 @@ const {
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
getMonitorAggregateStatsParamValidation,
getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
@@ -20,12 +18,15 @@ const SERVICE_NAME = "monitorController";
const { errorMessages, successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const logger = require("../utils/logger");
const { handleError, handleValidationError } = require("./controllerUtils");
/**
* Returns all monitors
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
@@ -38,51 +39,7 @@ const getAllMonitors = async (req, res, next) => {
data: monitors,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getAllMonitors") : null;
next(error);
}
};
/**
* Returns agregate stats for a monitor
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getMonitorAggregateStats = async (req, res, next) => {
try {
await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const { monitorId } = req.params;
const dateRange = req.query.dateRange;
const aggregateStats = await req.db.getMonitorAggregateStats(
monitorId,
dateRange
);
return res.json({
success: true,
msg: successMessages.MONTIOR_STATS_BY_ID,
data: aggregateStats,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMonitorAggregateStats")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
}
};
@@ -91,6 +48,7 @@ const getMonitorAggregateStats = async (req, res, next) => {
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
@@ -99,10 +57,7 @@ const getMonitorStatsById = async (req, res, next) => {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -110,13 +65,11 @@ const getMonitorStatsById = async (req, res, next) => {
const monitorStats = await req.db.getMonitorStatsById(req);
return res.json({
success: true,
msg: successMessages.MONTIOR_STATS_BY_ID,
msg: successMessages.MONITOR_STATS_BY_ID,
data: monitorStats,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorStatsById") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
}
};
@@ -124,10 +77,7 @@ const getMonitorCertificate = async (req, res, next) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
}
try {
@@ -151,31 +101,27 @@ const getMonitorCertificate = async (req, res, next) => {
});
}
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMonitorCertificate")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
}
};
/**
* Returns monitor with matching ID
* Retrieves a monitor by its ID.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the retrieved monitor data.
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
*/
const getMonitorById = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -184,45 +130,39 @@ const getMonitorById = async (req, res, next) => {
if (!monitor) {
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
error.status = 404;
throw error;
}
return res.json({
success: true,
msg: successMessages.MONTIOR_GET_BY_ID,
msg: successMessages.MONITOR_GET_BY_ID,
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorById") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorById"));
}
};
/**
* Returns all monitors and a sumamry for a team with TeamID
* Retrieves all monitors and a summary for a team based on the team ID.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @property {string} req.query.type - The type of the request.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
try {
await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(
req.params
);
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
//validation
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.method === undefined &&
error.method === "getMonitorsAndSummaryByTeamId";
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -239,32 +179,28 @@ const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
data: monitorsSummary,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMonitorsAndSummaryByTeamId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
/**
* Returns all monitors belong to team with TeamID
* Retrieves all monitors associated with a team by the team's ID.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -277,31 +213,27 @@ const getMonitorsByTeamId = async (req, res, next) => {
data: monitors,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorsByTeamId") : null;
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
next(error);
}
};
/**
* Creates a new monitor
* Creates a new monitor and adds it to the job queue.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications associated with the monitor.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const createMonitor = async (req, res, next) => {
try {
await createMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -310,13 +242,12 @@ const createMonitor = async (req, res, next) => {
const monitor = await req.db.createMonitor(req, res);
if (notifications && notifications.length !== 0) {
const setNotifications = await Promise.all(
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
await req.db.createNotification(notification);
})
);
monitor.notifications = setNotifications;
await monitor.save();
}
// Add monitor to job queue
@@ -327,62 +258,71 @@ const createMonitor = async (req, res, next) => {
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "createMonitor"));
}
};
/**
* Delete a monitor by ID
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be deleted.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the deletion of the monitor.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
*/
const deleteMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitor = await req.db.deleteMonitor(req, res, next);
// Delete associated checks,alerts,and notifications
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deleteAlertByMonitorId(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
/**
* TODO
* We should remove all checks and alerts associated with this monitor
* when it is deleted so there is no orphaned data
* We also need to make sure to stop all running services for this monitor
*/
try {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
{
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
}
);
}
return res
.status(200)
.json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
}
};
const deleteAllMonitors = async (req, res) => {
/**
* Deletes all monitors associated with a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
* @throws {Error} If there is an error during the deletion process.
*/
const deleteAllMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const { monitors, deletedCount } = await req.db.deleteAllMonitors(teamId);
await monitors.forEach(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
@@ -396,30 +336,29 @@ const deleteAllMonitors = async (req, res) => {
.status(200)
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteAllMonitors") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
}
};
/**
* Edit a monitor by ID
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be edited.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications to be associated with the monitor.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const editMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -453,21 +392,26 @@ const editMonitor = async (req, res, next) => {
data: editedMonitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "editMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "editMonitor"));
}
};
/**
* Pauses or resumes a monitor based on its current state.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data.
* @throws {Error} If there is an error during the process.
*/
const pauseMonitor = async (req, res, next) => {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
}
try {
@@ -488,35 +432,43 @@ const pauseMonitor = async (req, res, next) => {
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "pauseMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
}
};
/**
* Adds demo monitors for a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added.
* @throws {Error} If there is an error during the process.
*/
const addDemoMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { _id, teamId } = jwt.verify(token, process.env.JWT_SECRET);
const { jwtSecret } = req.settingsService.getSettings();
const { _id, teamId } = jwt.verify(token, jwtSecret);
const demoMonitors = await req.db.addDemoMonitors(_id, teamId);
await demoMonitors.forEach(async (monitor) => {
await req.jobQueue.addJob(monitor._id, monitor);
});
await Promise.all(
demoMonitors.map((monitor) => req.jobQueue.addJob(monitor._id, monitor))
);
return res.status(200).json({
success: true,
message: successMessages.MONITOR_DEMO_ADDED,
data: demoMonitors.length,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "addDemoMonitors") : null;
next(error);
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};
module.exports = {
getAllMonitors,
getMonitorAggregateStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
@@ -1,129 +0,0 @@
const PageSpeedService = require("../service/pageSpeedService");
const { successMessages } = require("../utils/messages");
const pageSpeedService = new PageSpeedService();
const SERVICE_NAME = "pagespeedCheckController";
const {
getPageSpeedCheckParamValidation,
createPageSpeedCheckParamValidation,
createPageSpeedCheckBodyValidation,
deletePageSpeedCheckParamValidation,
} = require("../validation/joi");
/**
* Gets all PageSpeedChecks for a monitor
* @async
* @param {Express.Request} req - The request object
* @param {Express.Response} res - The response object
* @param {Function} next - The next middleware function
* @returns {Promise<Express.Response>}
*/
const getPageSpeedChecks = async (req, res, next) => {
try {
// Validate monitorId parameter
await getPageSpeedCheckParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
return res.status(200).json({ msg: "Hit getPageSpeedChecks" });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getPageSpeedChecks") : null;
next(error);
}
};
/**
* Creates a new PageSpeedCheck for a monitor
* @async
* @param {Express.Request} req - The request object
* @param {Express.Response} res - The response object
* @param {Function} next - The next middleware function
* @returns {Promise<Express.Response>}
*/
const createPageSpeedCheck = async (req, res, next) => {
try {
await createPageSpeedCheckParamValidation.validateAsync(req.params);
await createPageSpeedCheckBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const { monitorId } = req.params;
const { url } = req.body;
// Run the PageSpeed check
const pageSpeedResults = await pageSpeedService.runPageSpeedCheck(url);
// Extract categories scores
const categories = pageSpeedResults.lighthouseResult?.categories;
if (!categories) {
throw new Error("Categories not found in PageSpeed results");
}
const newPageSpeedCheck = await pageSpeedService.createPageSpeedCheck({
monitorId,
accessibility: (categories.accessibility?.score || 0) * 100,
bestPractices: (categories["best-practices"]?.score || 0) * 100,
seo: (categories.seo?.score || 0) * 100,
performance: (categories.performance?.score || 0) * 100,
});
return res.status(201).json({
msg: successMessages.CREATED,
data: newPageSpeedCheck,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createPageSpeedCheck") : null;
next(error);
}
};
/**
* Deletes all PageSpeedChecks for a monitor
* @async
* @param {Express.Request} req - The request object
* @param {Express.Response} res - The response object
* @param {Function} next - The next middleware function
* @returns {Promise<Express.Response>}
*/
const deletePageSpeedCheck = async (req, res, next) => {
try {
// Validate monitorId parameter
await deletePageSpeedCheckParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
return res.status(200).json({ msg: "Hit deletePageSpeedCheck" });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deletePageSpeedCheck") : null;
next(error);
}
};
module.exports = {
getPageSpeedChecks,
createPageSpeedCheck,
deletePageSpeedCheck,
};
+8 -14
View File
@@ -1,3 +1,5 @@
const { handleError } = require("./controllerUtils");
const SERVICE_NAME = "JobQueueController";
const getMetrics = async (req, res, next) => {
@@ -7,9 +9,7 @@ const getMetrics = async (req, res, next) => {
.status(200)
.json({ success: true, msg: "Metrics retrieved", data: metrics });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMetrics") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
}
};
@@ -19,9 +19,7 @@ const getJobs = async (req, res, next) => {
const jobs = await req.jobQueue.getJobStats();
return res.status(200).json({ jobs });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getJobs") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
}
};
@@ -31,21 +29,17 @@ const addJob = async (req, res, next) => {
await req.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.send("Added job");
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "addJob") : null;
next(error);
next(handleError(error, SERVICE_NAME, "addJob"));
return;
}
};
const obliterateQueue = async (req, res, next) => {
try {
const obliterated = await req.jobQueue.obliterate();
return res.status(200).send("Obliterated jobs");
await req.jobQueue.obliterate();
return res.status(200).send("Obliterated queue");
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "obliterateQueue") : null;
next(error);
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
return;
}
};
+45
View File
@@ -0,0 +1,45 @@
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "SettingsController";
const { updateAppSettingsBodyValidation } = require("../validation/joi");
const { handleValidationError, handleError } = require("./controllerUtils");
const getAppSettings = async (req, res, next) => {
try {
const settings = { ...(await req.settingsService.getSettings()) };
delete settings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.GET_APP_SETTINGS,
data: settings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAppSettings"));
}
};
const updateAppSettings = async (req, res, next) => {
try {
await updateAppSettingsBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await req.db.updateAppSettings(req.body);
const updatedSettings = { ...(await req.settingsService.reloadSettings()) };
delete updatedSettings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.UPDATE_APP_SETTINGS,
data: updatedSettings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
}
};
module.exports = {
getAppSettings,
updateAppSettings,
};
+2 -2
View File
@@ -20,8 +20,8 @@
// The rest of the code is the same, as all the `db` methods are standardized.
// **************************
const Monitor = require("../models/Monitor");
const UserModel = require("../models/user");
const Monitor = require("./models/Monitor");
const UserModel = require("./models/User");
const bcrypt = require("bcrypt");
let FAKE_MONITOR_DATA = [];
+81
View File
@@ -0,0 +1,81 @@
const mongoose = require("mongoose");
const AppSettingsSchema = mongoose.Schema(
{
apiBaseUrl: {
type: String,
required: true,
default: "http://localhost:5000/api/v1",
},
logLevel: {
type: String,
default: "debug",
enum: ["debug", "none", "error", "warn"],
},
clientHost: {
type: String,
required: true,
default: "http://localhost:5173",
},
jwtSecret: {
type: String,
required: true,
default: "my_secret",
},
dbType: {
type: String,
required: true,
default: "MongoDB",
},
dbConnectionString: {
type: String,
required: true,
default: "mongodb://localhost:27017/uptime_db",
},
redisHost: {
type: String,
required: true,
default: "127.0.0.1",
},
redisPort: {
type: Number,
default: "6379",
},
jwtTTL: {
type: String,
required: true,
default: "99d",
},
pagespeedApiKey: {
type: String,
default: "",
},
systemEmailHost: {
type: String,
default: "smtp.gmail.com",
},
systemEmailPort: {
type: Number,
default: 465,
},
systemEmailAddress: {
type: String,
default: "",
},
systemEmailPassword: {
type: String,
default: "",
},
singleton: {
type: Boolean,
required: true,
unique: true,
default: true,
},
},
{
timestamps: true,
}
);
module.exports = mongoose.model("AppSettings", AppSettingsSchema);
@@ -1,6 +1,6 @@
const mongoose = require("mongoose");
const EmailService = require("../service/emailService");
const Notification = require("../models/Notification");
const EmailService = require("../../service/emailService");
const Notification = require("./Notification");
/**
* Check Schema for MongoDB collection.
File renamed without changes.
@@ -6,7 +6,7 @@ const mongoose = require("mongoose");
* @typedef {Object} MaintenanceWindow
* @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable.
* @property {Boolean} active - Indicates whether the maintenance window is active.
* @property {Boolean} oneTime - Indicates whether the maintenance window is a one-time event.
* @property {Number} repeat - Indicates how often this maintenance window should repeat.
* @property {Date} start - The start date and time of the maintenance window.
* @property {Date} end - The end date and time of the maintenance window.
* @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`.
@@ -16,12 +16,12 @@ const mongoose = require("mongoose");
* let maintenanceWindow = new MaintenanceWindow({
* monitorId: monitorId,
* active: active,
* oneTime: oneTime,
* repeat: repeat,
* start: start,
* end: end,
* });
*
* if (oneTime) {
* if (repeat === 0) {
* maintenanceWindow.expiry = end;
* }
*
@@ -34,16 +34,20 @@ const MaintenanceWindow = mongoose.Schema(
ref: "Monitor",
immutable: true,
},
userId: {
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
ref: "Team",
immutable: true,
},
active: {
type: Boolean,
default: true,
},
oneTime: {
type: Boolean,
name: {
type: String,
},
repeat: {
type: Number,
},
start: {
type: Date,
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+40 -28
View File
@@ -1,5 +1,6 @@
const mongoose = require("mongoose");
const UserModel = require("../../models/user");
const UserModel = require("../models/User");
const AppSettings = require("../models/AppSettings");
//****************************************
// DB Connection
@@ -7,7 +8,16 @@ const UserModel = require("../../models/user");
const connect = async () => {
try {
await mongoose.connect(process.env.DB_CONNECTION_STRING);
const connectionString =
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
let appSettings = await AppSettings.find();
if (appSettings.length === 0) {
appSettings = new AppSettings({});
await appSettings.save();
}
console.log("Connected to MongoDB");
} catch (error) {
console.error("Failed to connect to MongoDB");
@@ -36,6 +46,8 @@ const {
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
} = require("./modules/userModule");
@@ -47,6 +59,7 @@ const {
const {
requestInviteToken,
getInviteToken,
getInviteTokenAndDelete,
} = require("./modules/inviteModule");
//****************************************
@@ -100,48 +113,50 @@ const {
updateChecksTTL,
} = require("./modules/checkModule");
//****************************************
// Alerts
//****************************************
const {
createAlert,
getAlertsByUserId,
getAlertsByMonitorId,
getAlertById,
editAlert,
deleteAlert,
deleteAlertByMonitorId,
} = require("./modules/alertModule");
//****************************************
// Maintenance Window
//****************************************
const {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
} = require("./modules/maintenaceWindowModule");
editMaintenanceWindowById,
} = require("./modules/maintenanceWindowModule");
//****************************************
// Notifications
//****************************************
const {
createNotification,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
} = require("./modules/notificationModule");
//****************************************
// AppSettings
//****************************************
const {
getAppSettings,
updateAppSettings,
} = require("./modules/settingsModule");
module.exports = {
connect,
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
requestInviteToken,
getInviteToken,
getInviteTokenAndDelete,
requestRecoveryToken,
validateRecoveryToken,
resetPassword,
@@ -163,24 +178,21 @@ module.exports = {
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
createAlert,
getAlertsByUserId,
getAlertsByMonitorId,
getAlertById,
editAlert,
deleteAlert,
deleteAlertByMonitorId,
deleteMonitorsByUserId,
createPageSpeedCheck,
getPageSpeedChecks,
deletePageSpeedChecksByMonitorId,
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowById,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
createNotification,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
getAppSettings,
updateAppSettings,
};
-140
View File
@@ -1,140 +0,0 @@
const Alert = require("../../../models/Alert");
/**
* Creates an Alert associated with a Monitor and User
* @async
* @param {Object} alertData
* @param {string} alertData.checkId
* @param {string} alert.monitorId
* @param {string} alert.userId
* @param {boolean} alert.status
* @param {string} alert.message
* @param {boolean} alert.notifiedStatus
* @param {boolean} alert.acknowledgeStatus
* @returns {<Promise<Alert>>}
* @throws {Error}
*/
const createAlert = async (alertData) => {
try {
const alert = await new Alert({ ...alertData }).save();
return alert;
} catch (error) {
throw error;
}
};
/**
* Gets all alerts a User has set
* @async
* @param {string} userId
* @returns {<Promise<Array<Alert>>}
* @throws {Error}
*/
const getAlertsByUserId = async (userId) => {
try {
const alerts = await Alert.find({ userId });
return alerts;
} catch (error) {
throw error;
}
};
/**
* Gets all alerts a for an associated Monitor
* @async
* @param {string} monitorId
* @returns {<Promise<Array<Alert>>}
* @throws {Error}
*/
const getAlertsByMonitorId = async (monitorId) => {
try {
const alerts = await Alert.find({ monitorId });
return alerts;
} catch (error) {
throw error;
}
};
/**
* Returns an alert with specified ID
* @async
* @param {string} alertId
* @returns {Promise<Alert>}
* @throws {Error}
*/
const getAlertById = async (alertId) => {
try {
const alert = await Alert.findById(alertId);
return alert;
} catch (error) {
throw error;
}
};
/**
* Returns an edited alert with the specified ID
* @async
* @param {string} alertId
* @param {Object} alertData
* @param {string} alertData.checkId
* @param {string} alertData.monitorId
* @param {string} alertData.userId
* @param {boolean} alertData.status
* @param {string} alertData.message
* @param {boolean} alertData.notifiedStatus
* @param {boolean} alertData.acknowledgeStatus
* @param {Express.Response} res
* @returns {Promise<Alert>>}
* @throws {Error}
*/
const editAlert = async (alertId, alertData) => {
try {
const editedAlert = await Alert.findByIdAndUpdate(alertId, alertData, {
new: true,
});
return editedAlert;
} catch (error) {
throw error;
}
};
/**
* Deletes an alert with the specified ID
* @async
* @param {string} alertId
* @throws {Error}
*/
const deleteAlert = async (alertId) => {
try {
const result = await Alert.findByIdAndDelete(alertId);
return result;
} catch (error) {
throw error;
}
};
/**
* Deletes alerts by monitor ID.
*
* @param {string} monitorId - The ID of the monitor.
* @returns {Promise} A promise that resolves to the result of the delete operation.
* @throws {Error} If an error occurs while deleting the alerts.
*/
const deleteAlertByMonitorId = async (monitorId) => {
try {
const result = await Alert.deleteMany({ monitorId });
return result;
} catch (error) {
throw error;
}
};
module.exports = {
createAlert,
getAlertsByUserId,
getAlertsByMonitorId,
getAlertById,
editAlert,
deleteAlert,
deleteAlertByMonitorId,
};
+3 -3
View File
@@ -1,6 +1,6 @@
const Check = require("../../../models/Check");
const Monitor = require("../../../models/Monitor");
const User = require("../../../models/user");
const Check = require("../../models/Check");
const Monitor = require("../../models/Monitor");
const User = require("../../models/User");
const logger = require("../../../utils/logger");
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
+29 -2
View File
@@ -1,4 +1,4 @@
const InviteToken = require("../../../models/InviteToken");
const InviteToken = require("../../models/InviteToken");
const crypto = require("crypto");
const { errorMessages } = require("../../../utils/messages");
const SERVICE_NAME = "inviteModule";
@@ -32,7 +32,7 @@ const requestInviteToken = async (userData) => {
};
/**
* Retrieves and deletes an invite token.
* Retrieves an invite token
*
* This function searches for an invite token in the database and deletes it.
* If the invite token is not found, it throws an error.
@@ -42,6 +42,32 @@ const requestInviteToken = async (userData) => {
* @throws {Error} If the invite token is not found or there is another error.
*/
const getInviteToken = async (token) => {
try {
const invite = await InviteToken.findOne({
token,
});
if (invite === null) {
throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteToken";
throw error;
}
};
/**
* Retrieves and deletes an invite token
*
* This function searches for an invite token in the database and deletes it.
* If the invite token is not found, it throws an error.
*
* @param {string} token - The invite token to search for.
* @returns {Promise<InviteToken>} The invite token data.
* @throws {Error} If the invite token is not found or there is another error.
*/
const getInviteTokenAndDelete = async (token) => {
try {
const invite = await InviteToken.findOneAndDelete({
token,
@@ -60,4 +86,5 @@ const getInviteToken = async (token) => {
module.exports = {
requestInviteToken,
getInviteToken,
getInviteTokenAndDelete,
};
@@ -1,4 +1,4 @@
const MaintenanceWindow = require("../../../models/MaintenanceWindow");
const MaintenanceWindow = require("../../models/MaintenanceWindow");
const SERVICE_NAME = "maintenanceWindowModule";
/**
@@ -36,7 +36,7 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
}
const result = maintenanceWindow.save();
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
@@ -45,22 +45,60 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
}
};
const getMaintenanceWindowById = async (maintenanceWindowId) => {
try {
const maintenanceWindow = await MaintenanceWindow.findById({
_id: maintenanceWindowId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
/**
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific user ID.
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific team ID.
* @async
* @function getMaintenanceWindowByUserId
* @param {String} userId - The ID of the user.
* @param {String} teamId - The ID of the team.
* @param {Object} query - The request body.
* @returns {Promise<Array<MaintenanceWindow>>} An array of MaintenanceWindow documents.
* @throws {Error} If there is an error retrieving the documents.
* @example
* getMaintenanceWindowByUserId('userId')
* getMaintenanceWindowByUserId(teamId)
* .then(maintenanceWindows => console.log(maintenanceWindows))
* .catch(error => console.error(error));
*/
const getMaintenanceWindowsByUserId = async (userId) => {
const getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
const maintenanceWindows = await MaintenanceWindow.find({ userId: userId });
return maintenanceWindows;
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount =
await MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery)
.skip(skip)
.limit(rowsPerPage)
.sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
@@ -96,23 +134,23 @@ const getMaintenanceWindowsByMonitorId = async (monitorId) => {
/**
* Asynchronously deletes a MaintenanceWindow document by its ID.
* @async
* @function deleteMaintenaceWindowById
* @function deleteMaintenanceWindowById
* @param {mongoose.Schema.Types.ObjectId} maintenanceWindowId - The ID of the MaintenanceWindow document to delete.
* @returns {Promise<MaintenanceWindow>} The deleted MaintenanceWindow document.
* @throws {Error} If there is an error deleting the document.
* @example
* deleteMaintenaceWindowById('maintenanceWindowId')
* deleteMaintenanceWindowById('maintenanceWindowId')
* .then(maintenanceWindow => console.log(maintenanceWindow))
* .catch(error => console.error(error));
*/
const deleteMaintenaceWindowById = async (maintenanceWindowId) => {
const deleteMaintenanceWindowById = async (maintenanceWindowId) => {
try {
const maintenanceWindow =
await MaintenanceWindow.findByIdAndDelete(maintenanceWindowId);
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenaceWindowById";
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
@@ -163,11 +201,32 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
}
};
const editMaintenanceWindowById = async (
maintenanceWindowId,
maintenanceWindowData
) => {
console.log(maintenanceWindowData);
try {
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
maintenanceWindowId,
maintenanceWindowData,
{ new: true }
);
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
module.exports = {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
};
+7 -7
View File
@@ -1,8 +1,8 @@
const Monitor = require("../../../models/Monitor");
const Check = require("../../../models/Check");
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
const Monitor = require("../../models/Monitor");
const Check = require("../../models/Check");
const PageSpeedCheck = require("../../models/PageSpeedCheck");
const { errorMessages } = require("../../../utils/messages");
const Notification = require("../../../models/Notification");
const Notification = require("../../models/Notification");
const { NormalizeData } = require("../../../utils/dataUtils");
const demoMonitors = require("../../../utils/demoMonitors.json");
const SERVICE_NAME = "monitorModule";
@@ -147,7 +147,7 @@ const getMonitorStatsById = async (req) => {
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
}
// This effectively removes limit, returning all checks
if (limit === undefined) limit = 0;
@@ -283,7 +283,7 @@ const getMonitorById = async (monitorId) => {
try {
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
}
// Get notifications
const notifications = await Notification.find({
@@ -481,7 +481,7 @@ const deleteMonitor = async (req, res) => {
try {
const monitor = await Monitor.findByIdAndDelete(monitorId);
if (!monitor) {
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
}
return monitor;
} catch (error) {
@@ -1,4 +1,4 @@
const Notification = require("../../../models/Notification");
const Notification = require("../../models/Notification");
const SERVICE_NAME = "notificationModule";
/**
* Creates a new notification.
@@ -1,4 +1,4 @@
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
const PageSpeedCheck = require("../../models/PageSpeedCheck");
const SERVICE_NAME = "pageSpeedCheckModule";
/**
* Create a PageSpeed check for a monitor
+2 -2
View File
@@ -1,5 +1,5 @@
const UserModel = require("../../../models/user");
const RecoveryToken = require("../../../models/RecoveryToken");
const UserModel = require("../../models/User");
const RecoveryToken = require("../../models/RecoveryToken");
const crypto = require("crypto");
const { errorMessages } = require("../../../utils/messages");
const SERVICE_NAME = "recoveryModule";
+33
View File
@@ -0,0 +1,33 @@
const AppSettings = require("../../models/AppSettings");
const SERVICE_NAME = "SettingsModule";
const getAppSettings = async () => {
try {
const settings = AppSettings.findOne();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getSettings";
throw error;
}
};
const updateAppSettings = async (newSettings) => {
try {
const settings = await AppSettings.findOneAndUpdate(
{},
{ $set: newSettings },
{ new: true }
);
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateAppSettings";
throw error;
}
};
module.exports = {
getAppSettings,
updateAppSettings,
};
+32 -2
View File
@@ -1,5 +1,5 @@
const UserModel = require("../../../models/user");
const TeamModel = require("../../../models/Team");
const UserModel = require("../../models/User");
const TeamModel = require("../../models/Team");
const { errorMessages } = require("../../../utils/messages");
const { GenerateAvatarImage } = require("../../../utils/imageProcessing");
@@ -35,6 +35,7 @@ const insertUser = async (userData, imageFile) => {
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
}
@@ -156,6 +157,33 @@ const deleteUser = async (userId) => {
}
};
/**
* Delete a user by ID
* @async
* @param {string} teamId
* @returns {void}
* @throws {Error}
*/
const deleteTeam = async (teamId) => {
try {
await TeamModel.findByIdAndDelete(teamId);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteTeam";
throw error;
}
};
const deleteAllOtherUsers = async () => {
try {
await UserModel.deleteMany({ role: { $ne: "superadmin" } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllOtherUsers";
throw error;
}
};
const getAllUsers = async (req, res) => {
try {
const users = await UserModel.find()
@@ -185,6 +213,8 @@ module.exports = {
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
};
+30 -17
View File
@@ -1,7 +1,10 @@
const path = require("path");
const fs = require("fs");
const swaggerUi = require("swagger-ui-express");
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
require("dotenv").config();
const logger = require("./utils/logger");
const { verifyJWT } = require("./middleware/verifyJWT");
const { handleErrors } = require("./middleware/handleErrors");
@@ -10,19 +13,21 @@ const authRouter = require("./routes/authRoute");
const inviteRouter = require("./routes/inviteRoute");
const monitorRouter = require("./routes/monitorRoute");
const checkRouter = require("./routes/checkRoute");
const alertRouter = require("./routes/alertRoute");
const pageSpeedCheckRouter = require("./routes/pageSpeedCheckRoute");
const maintenanceWindowRouter = require("./routes/maintenanceWindowRoute");
const settingsRouter = require("./routes/settingsRoute");
const { connectDbAndRunServer } = require("./configs/db");
const queueRouter = require("./routes/queueRoute");
const JobQueue = require("./service/jobQueue");
const NetworkService = require("./service/networkService");
const EmailService = require("./service/emailService");
const PageSpeedService = require("./service/pageSpeedService");
const SettingsService = require("./service/settingsService");
const SERVICE_NAME = "Server";
let cleaningUp = false;
let cleaningUp = false;
const openApiSpec = JSON.parse(
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
);
// Need to wrap server setup in a function to handle async nature of JobQueue
const startApp = async () => {
// **************************
@@ -42,9 +47,11 @@ const startApp = async () => {
FakedDB: () => require("./db/FakeDb"),
};
const db = DB_TYPE[process.env.DB_TYPE]
? DB_TYPE[process.env.DB_TYPE]()
: require("./db/FakeDb");
// const db = DB_TYPE[process.env.DB_TYPE]
// ? DB_TYPE[process.env.DB_TYPE]()
// : require("./db/FakeDb");
const db = DB_TYPE.MongoDB();
const app = express();
@@ -55,7 +62,6 @@ const startApp = async () => {
);
app.use(express.json());
app.use(helmet());
// **************************
// Make DB accessible anywhere we have a Request object
// By adding the DB to the request object, we can access it in any route
@@ -66,20 +72,21 @@ const startApp = async () => {
req.db = db;
req.jobQueue = jobQueue;
req.emailService = emailService;
req.pageSpeedService = pageSpeedService;
req.settingsService = settingsService;
next();
});
// Swagger UI
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
//routes
app.use("/api/v1/auth", authRouter);
app.use("/api/v1/settings", verifyJWT, settingsRouter);
app.use("/api/v1/invite", inviteRouter);
app.use("/api/v1/monitors", verifyJWT, monitorRouter);
app.use("/api/v1/checks", verifyJWT, checkRouter);
app.use("/api/v1/alerts", verifyJWT, alertRouter);
app.use("/api/v1/pagespeed", verifyJWT, pageSpeedCheckRouter);
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRouter);
//Temporary route for testing, remove later
app.use("/api/v1/job", queueRouter);
app.use("/api/v1/queue", verifyJWT, queueRouter);
//health check
app.use("/api/v1/healthy", (req, res) => {
@@ -117,10 +124,16 @@ const startApp = async () => {
// Create services
await connectDbAndRunServer(app, db);
const emailService = new EmailService();
const settingsService = new SettingsService();
await settingsService.loadSettings();
const emailService = new EmailService(settingsService);
const networkService = new NetworkService(db, emailService);
const jobQueue = await JobQueue.createJobQueue(db, networkService);
const pageSpeedService = new PageSpeedService();
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService
);
const cleanup = async () => {
if (cleaningUp) {
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more