├── .dockerignore ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── unit-test-ci.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .lintstagedrc.json ├── .nvmrc ├── .oxlintrc.json ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── components │ ├── home │ │ ├── menu │ │ │ ├── index.jsx │ │ │ ├── layout.jsx │ │ │ ├── mobile.jsx │ │ │ ├── mobile │ │ │ │ ├── layout.jsx │ │ │ │ └── status.jsx │ │ │ ├── status.jsx │ │ │ └── styles.scss │ │ └── monitor │ │ │ ├── index.jsx │ │ │ ├── layout │ │ │ ├── card.jsx │ │ │ ├── card.scss │ │ │ ├── compact │ │ │ │ ├── index.jsx │ │ │ │ ├── monitor.jsx │ │ │ │ └── styles.scss │ │ │ ├── list.jsx │ │ │ ├── list.scss │ │ │ ├── table.jsx │ │ │ └── table.scss │ │ │ └── options.jsx │ ├── icons │ │ ├── index.jsx │ │ └── statusLogo.jsx │ ├── modal │ │ ├── monitor │ │ │ ├── configure.jsx │ │ │ ├── delete.jsx │ │ │ ├── pages │ │ │ │ ├── body.jsx │ │ │ │ ├── headers.jsx │ │ │ │ ├── http │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── methods.jsx │ │ │ │ │ └── statusCodes.jsx │ │ │ │ ├── initial │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── type.jsx │ │ │ │ ├── interval.jsx │ │ │ │ ├── notification │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── list.jsx │ │ │ │ │ └── type.jsx │ │ │ │ └── tcp.jsx │ │ │ └── styles.scss │ │ ├── notification │ │ │ ├── delete.jsx │ │ │ ├── dropdown │ │ │ │ ├── icon.jsx │ │ │ │ ├── platform.jsx │ │ │ │ └── type.jsx │ │ │ ├── index.jsx │ │ │ ├── payload.jsx │ │ │ ├── platform │ │ │ │ ├── discord.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── slack.jsx │ │ │ │ ├── telegram.jsx │ │ │ │ └── webhook.jsx │ │ │ └── styles.scss │ │ ├── settings │ │ │ ├── account │ │ │ │ ├── avatar.jsx │ │ │ │ ├── avatar.scss │ │ │ │ ├── delete.jsx │ │ │ │ ├── password.jsx │ │ │ │ ├── transfer.jsx │ │ │ │ └── username.jsx │ │ │ ├── avatar.jsx │ │ │ ├── avatars.scss │ │ │ └── manage │ │ │ │ ├── approve.jsx │ │ │ │ ├── decline.jsx │ │ │ │ ├── delete.jsx │ │ │ │ ├── permissions.jsx │ │ │ │ └── permissions.scss │ │ └── status │ │ │ ├── configure │ │ │ ├── add.jsx │ │ │ ├── add.scss │ │ │ ├── reorder.jsx │ │ │ └── reorder.scss │ │ │ ├── delete.jsx │ │ │ └── index.jsx │ ├── monitor │ │ ├── graph │ │ │ ├── graph.scss │ │ │ ├── index.jsx │ │ │ ├── menu.jsx │ │ │ └── ping.jsx │ │ ├── menu.jsx │ │ ├── menu.scss │ │ ├── status.jsx │ │ ├── status.scss │ │ ├── updateInfo.jsx │ │ ├── uptime.jsx │ │ └── uptime.scss │ ├── navigation │ │ ├── index.jsx │ │ ├── index.scss │ │ ├── left.jsx │ │ ├── left.scss │ │ ├── top.jsx │ │ └── top.scss │ ├── notifications │ │ ├── layout │ │ │ ├── card.jsx │ │ │ ├── card.scss │ │ │ ├── compact.jsx │ │ │ └── compact.scss │ │ └── menu │ │ │ ├── index.jsx │ │ │ ├── layout.jsx │ │ │ ├── mobile.jsx │ │ │ ├── platform.jsx │ │ │ └── styles.scss │ ├── register │ │ ├── checklist.jsx │ │ ├── checklist.scss │ │ ├── email.jsx │ │ ├── password.jsx │ │ └── verify.jsx │ ├── settings │ │ ├── about.jsx │ │ ├── about.scss │ │ ├── account │ │ │ ├── avatar.jsx │ │ │ ├── avatar.scss │ │ │ ├── index.jsx │ │ │ ├── item │ │ │ │ ├── desktop.jsx │ │ │ │ ├── desktop.scss │ │ │ │ ├── mobile.jsx │ │ │ │ └── mobile.scss │ │ │ └── style.scss │ │ ├── manage │ │ │ ├── index.jsx │ │ │ ├── member │ │ │ │ ├── actions.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── row.jsx │ │ │ │ ├── row.scss │ │ │ │ └── style.scss │ │ │ └── style.scss │ │ ├── personalisation │ │ │ ├── accent.jsx │ │ │ ├── appearance.jsx │ │ │ ├── dateformat.jsx │ │ │ ├── index.jsx │ │ │ ├── style.scss │ │ │ ├── theme.jsx │ │ │ ├── timeformat.jsx │ │ │ └── timezone.jsx │ │ └── ui │ │ │ ├── menu │ │ │ ├── desktop.jsx │ │ │ └── mobile.jsx │ │ │ └── tab │ │ │ ├── desktop.jsx │ │ │ ├── mobile.jsx │ │ │ └── tab.scss │ ├── setup │ │ ├── database.jsx │ │ ├── dropdown.jsx │ │ ├── index.jsx │ │ ├── style.scss │ │ └── type.jsx │ ├── status │ │ ├── configure │ │ │ ├── appearance │ │ │ │ ├── branding.jsx │ │ │ │ ├── color.jsx │ │ │ │ ├── fonts.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── styles.scss │ │ │ ├── layout │ │ │ │ ├── customCSS.jsx │ │ │ │ ├── customHTML.jsx │ │ │ │ ├── header │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── logo.jsx │ │ │ │ │ ├── logoOptions.jsx │ │ │ │ │ ├── status.jsx │ │ │ │ │ ├── statusOptions.jsx │ │ │ │ │ └── styles.scss │ │ │ │ ├── history │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── list.jsx │ │ │ │ │ └── styles.scss │ │ │ │ ├── incidents │ │ │ │ │ ├── design │ │ │ │ │ │ ├── basic.jsx │ │ │ │ │ │ ├── pretty.jsx │ │ │ │ │ │ └── simple.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── styles.scss │ │ │ │ ├── index.jsx │ │ │ │ ├── metrics │ │ │ │ │ ├── area.jsx │ │ │ │ │ ├── dropdown.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── options.jsx │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── type │ │ │ │ │ │ ├── basic.jsx │ │ │ │ │ │ ├── chart │ │ │ │ │ │ ├── area.jsx │ │ │ │ │ │ └── line.jsx │ │ │ │ │ │ ├── nerdy.jsx │ │ │ │ │ │ ├── pretty.jsx │ │ │ │ │ │ └── styles.scss │ │ │ │ ├── status.jsx │ │ │ │ ├── status.scss │ │ │ │ ├── styles.scss │ │ │ │ └── uptime │ │ │ │ │ ├── graph │ │ │ │ │ ├── basic.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── nerdy.jsx │ │ │ │ │ ├── pretty.jsx │ │ │ │ │ └── styles.scss │ │ │ │ │ └── index.jsx │ │ │ ├── monitor │ │ │ │ ├── index.jsx │ │ │ │ ├── item.jsx │ │ │ │ └── styles.scss │ │ │ ├── preview │ │ │ │ ├── footer.jsx │ │ │ │ ├── header │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── status.jsx │ │ │ │ │ ├── style.scss │ │ │ │ │ └── title.jsx │ │ │ │ ├── incident.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── metrics │ │ │ │ │ ├── dropdown.jsx │ │ │ │ │ ├── graph │ │ │ │ │ │ ├── basic.jsx │ │ │ │ │ │ ├── nerdy.jsx │ │ │ │ │ │ └── pretty.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── separate.jsx │ │ │ │ │ └── style.scss │ │ │ │ ├── status.jsx │ │ │ │ ├── styles.scss │ │ │ │ └── uptime.jsx │ │ │ └── settings │ │ │ │ └── index.jsx │ │ ├── index.jsx │ │ └── styles.scss │ └── ui │ │ ├── accordion │ │ ├── accordion.jsx │ │ ├── accordion.scss │ │ ├── context.js │ │ ├── index.jsx │ │ └── item.jsx │ │ ├── alert │ │ ├── alert.scss │ │ ├── error.jsx │ │ ├── index.jsx │ │ ├── info.jsx │ │ ├── success.jsx │ │ └── warning.jsx │ │ ├── avatar.jsx │ │ ├── avatar.scss │ │ ├── button.jsx │ │ ├── button.scss │ │ ├── checkbox.jsx │ │ ├── checkbox.scss │ │ ├── colorPicker.jsx │ │ ├── colorPicker.scss │ │ ├── dropdown │ │ ├── container.jsx │ │ ├── dropdown.scss │ │ ├── index.jsx │ │ ├── item.jsx │ │ ├── list.jsx │ │ └── trigger.jsx │ │ ├── input.jsx │ │ ├── input.scss │ │ ├── modal │ │ ├── actions.jsx │ │ ├── button.jsx │ │ ├── container.jsx │ │ ├── index.jsx │ │ ├── message.jsx │ │ ├── style.scss │ │ └── title.jsx │ │ ├── progress.jsx │ │ ├── progress.scss │ │ ├── searchBar.jsx │ │ ├── searchBar.scss │ │ ├── select │ │ ├── container.jsx │ │ ├── index.jsx │ │ ├── item.jsx │ │ ├── list.jsx │ │ ├── select.scss │ │ └── trigger.jsx │ │ ├── spacer.jsx │ │ ├── statusBar.jsx │ │ ├── statusBar.scss │ │ ├── table │ │ ├── body.jsx │ │ ├── caption.jsx │ │ ├── cell.jsx │ │ ├── footer.jsx │ │ ├── head.jsx │ │ ├── header.jsx │ │ ├── index.jsx │ │ ├── row.jsx │ │ ├── styles.scss │ │ └── table.jsx │ │ ├── tabs.jsx │ │ ├── textarea.jsx │ │ ├── textarea.scss │ │ ├── tooltip.jsx │ │ └── tooltip.scss ├── constant │ ├── dateformats.json │ ├── notifications.json │ ├── status.js │ ├── statusCodes.json │ └── timezones.json ├── context │ ├── global.js │ ├── index.js │ ├── modal.js │ ├── notifications.js │ ├── status.js │ ├── team.js │ └── user.js ├── handlers │ ├── login.js │ ├── monitor.js │ ├── register.js │ ├── settings │ │ └── account │ │ │ ├── password.js │ │ │ ├── transfer.js │ │ │ └── username.js │ ├── setup.js │ └── status │ │ ├── configure │ │ └── create.js │ │ └── index.js ├── hooks │ ├── useConfigureStatus.jsx │ ├── useDropdown.jsx │ ├── useGoBack.jsx │ ├── useGraphStatus.jsx │ ├── useLocalstorage.jsx │ ├── useLogin.jsx │ ├── useMonitorForm.jsx │ ├── useNotificationForm.jsx │ ├── useRegister.jsx │ ├── useSelect.jsx │ └── useSetup.jsx ├── layout │ ├── global.jsx │ └── status.jsx ├── main.jsx ├── pages │ ├── error.jsx │ ├── home.jsx │ ├── login.jsx │ ├── monitor.jsx │ ├── notifications.jsx │ ├── register.jsx │ ├── settings.jsx │ ├── setup.jsx │ ├── status-pages │ │ ├── configure.jsx │ │ └── index.jsx │ ├── status.jsx │ └── verify.jsx ├── services │ ├── axios.js │ └── monitor │ │ └── fetch.js └── styles │ ├── breakpoints.scss │ ├── colors.scss │ ├── font.scss │ ├── pages │ ├── error.scss │ ├── home.scss │ ├── monitor.scss │ ├── notifications.scss │ ├── register.scss │ ├── settings.scss │ ├── status │ │ └── configure.scss │ └── verify.scss │ ├── pxToRem.scss │ ├── radius.scss │ ├── reset.scss │ ├── shadows.scss │ ├── styles.scss │ ├── themes.scss │ └── transitions.scss ├── cypress.config.js ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.js │ │ └── style.css ├── api │ ├── monitor.md │ ├── notification.md │ └── user.md ├── components │ ├── DividePage.vue │ ├── guides.vue │ └── kanban │ │ ├── badge.vue │ │ ├── index.vue │ │ └── taskCard.vue ├── configuration.md ├── contributing │ ├── conduct.md │ ├── issues.md │ ├── overview.md │ ├── pull-request.md │ └── testing.md ├── getting-started.md ├── guides │ ├── discord │ │ └── create-webhook.md │ ├── index.md │ ├── slack │ │ └── create-webhook.md │ ├── telegram │ │ ├── create-bot.md │ │ └── find-chat-id.md │ └── webhook │ │ └── index.md ├── index.md ├── internals │ ├── changelog.md │ ├── components.md │ ├── config.md │ ├── flows.md │ ├── notifications.md │ ├── overview.md │ ├── permissions.md │ └── roadmap.md ├── intro.md ├── kanban │ ├── header.md │ └── notifications.md └── public │ ├── demo.gif │ ├── guides │ ├── Discord_Integration.webp │ ├── Discord_Webhook.webp │ ├── Lunalytics_Add_Monitor_Notification.webp │ ├── Lunalytics_Create_Monitor.webp │ ├── Lunalytics_Create_Notification.webp │ └── Lunalytics_Discord_Create_Notification.webp │ ├── header.png │ ├── icon-192x192.png │ ├── icon-512x512.png │ └── manifest.json ├── eslint.config.mjs ├── index.html ├── package-lock.json ├── package.json ├── public ├── LogoWithName.png ├── icon-192x192.png ├── icon-512x512.png ├── icons │ ├── Ape.png │ ├── Bear.png │ ├── Cat.png │ ├── Dog.png │ ├── Duck.png │ ├── Eagle.png │ ├── Fox.png │ ├── Gerbil.png │ ├── Hamster.png │ ├── Hedgehog.png │ ├── Koala.png │ ├── Ostrich.png │ ├── Panda.png │ ├── Rabbit.png │ ├── Rocket.png │ ├── Smart-Dog.png │ └── Tiger.png ├── logo.svg ├── logo │ ├── postgresql.svg │ └── sqlite.svg ├── manifest.json ├── meme │ └── cat.png ├── notifications │ ├── discord.svg │ ├── slack.svg │ ├── telegram.svg │ └── webhook.svg └── robots.txt ├── scripts ├── migrate.js ├── migrate_manual.js ├── migrations │ ├── 0-4-0.js │ ├── 0-6-0.js │ ├── 0-6-5.js │ ├── 0-7-0.js │ ├── 0-7-2.js │ ├── 0-8-0.js │ └── index.js ├── reset.js └── setup.js ├── server ├── cache │ ├── index.js │ └── status.js ├── class │ ├── certificate.js │ ├── monitor.js │ ├── notification.js │ └── status.js ├── database │ ├── queries │ │ ├── api.js │ │ ├── certificate.js │ │ ├── heartbeat.js │ │ ├── monitor.js │ │ ├── notification.js │ │ ├── session.js │ │ ├── status.js │ │ └── user.js │ └── sqlite │ │ └── setup.js ├── index.js ├── middleware │ ├── auth │ │ ├── emailExists.js │ │ ├── index.js │ │ ├── login.js │ │ ├── logout.js │ │ ├── register.js │ │ └── setup.js │ ├── authorization.js │ ├── declineApiAccess.js │ ├── demo.js │ ├── monitor │ │ ├── add.js │ │ ├── delete.js │ │ ├── edit.js │ │ ├── id.js │ │ ├── pause.js │ │ └── status.js │ ├── notifications │ │ ├── create.js │ │ ├── delete.js │ │ ├── disable.js │ │ ├── edit.js │ │ ├── getAll.js │ │ └── getUsingId.js │ ├── setupExists.js │ ├── status │ │ ├── create.js │ │ ├── defaultPage.js │ │ ├── delete.js │ │ ├── edit.js │ │ ├── getAll.js │ │ ├── getUsingId.js │ │ └── statusPageUsingId.js │ └── user │ │ ├── access │ │ ├── approveUser.js │ │ ├── declineUser.js │ │ └── removeUser.js │ │ ├── deleteAccount.js │ │ ├── exists.js │ │ ├── hasPermission.js │ │ ├── monitors.js │ │ ├── permission │ │ └── update.js │ │ ├── team │ │ └── members.js │ │ ├── transferOwnership.js │ │ ├── update │ │ ├── avatar.js │ │ ├── password.js │ │ └── username.js │ │ └── user.js ├── notifications │ ├── base.js │ ├── discord.js │ ├── index.js │ ├── slack.js │ ├── telegram.js │ └── webhook.js ├── routes │ ├── auth.js │ ├── index.js │ ├── monitor.js │ ├── notifications.js │ ├── statusApi.js │ ├── statusPages.js │ └── user.js ├── tools │ ├── checkCertificate.js │ ├── httpStatus.js │ └── tcpPing.js └── utils │ ├── config.js │ ├── cron.js │ ├── errors.js │ ├── hashPassword.js │ ├── jwt.js │ ├── logger.js │ ├── randomId.js │ ├── status.js │ └── uaParser.js ├── shared ├── constants │ └── status.js ├── data │ └── setup.js ├── notifications │ ├── discord.js │ ├── index.js │ ├── replacers │ │ ├── date.js │ │ └── notification.js │ ├── slack.js │ ├── telegram.js │ └── webhook.js ├── parseJson.js ├── permissions │ ├── bitFlags.js │ ├── oldPermsToFlags.js │ ├── role.js │ ├── user.js │ └── validate.js ├── schema │ └── status │ │ ├── customCSS.js │ │ ├── customHTML.js │ │ ├── header.js │ │ ├── history.js │ │ ├── incidents.js │ │ ├── index.js │ │ ├── metrics.js │ │ ├── status.js │ │ └── uptime.js ├── utils │ ├── collection.js │ ├── cookies.js │ ├── errors.js │ ├── ms.js │ ├── object.js │ ├── propTypes.js │ ├── schema.js │ └── url.js └── validators │ ├── auth.js │ ├── index.js │ ├── monitor.js │ ├── notifications │ ├── discord.js │ ├── index.js │ ├── slack.js │ ├── telegram.js │ └── webhook.js │ ├── setup.js │ ├── status │ ├── layout.js │ └── settings.js │ └── user.js ├── test ├── e2e │ ├── monitor │ │ ├── http.test.js │ │ └── tcp.test.js │ ├── notification │ │ ├── discord.test.js │ │ ├── slack.test.js │ │ ├── telegram.test.js │ │ └── webhooks.test.js │ ├── register.test.js │ ├── setup │ │ ├── fixtures │ │ │ ├── login.json │ │ │ ├── monitor.json │ │ │ └── notifications │ │ │ │ ├── discord.json │ │ │ │ ├── slack.json │ │ │ │ ├── telegram.json │ │ │ │ └── webhooks.json │ │ └── support │ │ │ ├── commands.js │ │ │ └── e2e.js │ ├── signin.test.js │ └── verify.test.js ├── server │ ├── classes │ │ ├── certificate.test.js │ │ └── monitor.test.js │ ├── database │ │ └── certificate.test.js │ └── middleware │ │ ├── auth │ │ ├── login.test.js │ │ ├── logout.test.js │ │ └── register.test.js │ │ ├── authorization.test.js │ │ ├── monitor │ │ ├── add.test.js │ │ ├── delete.test.js │ │ ├── edit.test.js │ │ ├── id.test.js │ │ └── status.test.js │ │ ├── setup.test.js │ │ └── user │ │ ├── access │ │ ├── approve.test.js │ │ ├── decline.test.js │ │ └── removeUser.test.js │ │ ├── deleteAccount.test.js │ │ ├── hasPermission.test.js │ │ ├── permission │ │ └── update.test.js │ │ ├── team │ │ └── member.test.js │ │ ├── transferOwnership.test.js │ │ └── update │ │ ├── avatar.test.js │ │ ├── password.test.js │ │ └── username.test.js └── shared │ └── setupDatabase.js ├── vite.config.js └── vitest.config.mjs /.dockerignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /data 3 | /dist 4 | /docker 5 | /docs 6 | /logs 7 | /node_modules 8 | /stats 9 | *.db 10 | /test 11 | **/.dockerignore 12 | **/.git 13 | **/.gitignore 14 | **/docker-compose* 15 | **/[Dd]ockerfile* 16 | LICENSE 17 | README.md 18 | .editorconfig 19 | .lintstagedrc.json 20 | .oxlintrc.json 21 | .prettier* 22 | .eslint* 23 | /.github 24 | yarn.lock 25 | CODE_OF_CONDUCT.md 26 | CONTRIBUTING.md 27 | SECURITY.md 28 | .env 29 | /tmp 30 | config.json 31 | 32 | #node_modules 33 | .DS_Store 34 | #dist 35 | dist 36 | *.local 37 | #.idea 38 | 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | ## New Features 4 | 5 | ## Updates 6 | 7 | ## Additional Info 8 | 9 | - [ ] Does this update require migration? (If yes, add extra details) 10 | - [ ] Are there any other PRs that need to be merged? (If yes, add extra details) 11 | -------------------------------------------------------------------------------- /.github/workflows/unit-test-ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit test CI 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm run test:app 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /data 11 | /dist 12 | /stats 13 | /app/pages/icons.jsx 14 | /app/components/icons/list.jsx 15 | docs/.vitepress/dist 16 | docs/.vitepress/cache 17 | public/kanban.json 18 | /logs 19 | *.db 20 | test/e2e/setup/downloads 21 | /docker 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | yarn.lock 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env 35 | config.json 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # Editor directories and files 42 | .vscode/* 43 | !.vscode/extensions.json 44 | .idea 45 | .DS_Store 46 | *.suo 47 | *.ntvs* 48 | *.njsproj 49 | *.sln 50 | *.sw? 51 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test:app 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": ["prettier --write", "eslint"], 3 | "*.scss": "prettier --write" 4 | } 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.14.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | .contentlayer -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "importOrder": [ 8 | "^(react/(.*)$)|^(react$)", 9 | "", 10 | "", 11 | "^types$", 12 | "^@/env(.*)$", 13 | "^@/types/(.*)$", 14 | "^@/config/(.*)$", 15 | "^@/lib/(.*)$", 16 | "^@/app/hooks/(.*)$", 17 | "^@/app/components/ui/(.*)$", 18 | "^@/app/components/(.*)$", 19 | "^@/app/styles/(.*)$", 20 | "^@/app/(.*)$", 21 | "", 22 | "^[./]" 23 | ], 24 | "importOrderSeparation": false, 25 | "importOrderSortSpecifiers": true, 26 | "importOrderBuiltinModulesToTop": true, 27 | "importOrderParserPlugins": ["jsx", "decorators-legacy"], 28 | "importOrderMergeDuplicateImports": true, 29 | "importOrderCombineTypeAndValueImports": true 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDPLATFORM 2 | 3 | FROM --platform=$BUILDPLATFORM node:22.14.0-alpine AS base 4 | 5 | RUN apk add --no-cache \ 6 | build-base \ 7 | python3 \ 8 | py3-pip \ 9 | make \ 10 | g++ 11 | 12 | WORKDIR /app 13 | 14 | FROM base AS builder 15 | 16 | COPY package*.json ./ 17 | RUN npm ci 18 | 19 | COPY . . 20 | 21 | RUN npm run build 22 | 23 | FROM node:22.14.0-alpine AS production 24 | 25 | RUN apk add --no-cache \ 26 | python3 \ 27 | make \ 28 | g++ 29 | 30 | WORKDIR /app 31 | 32 | COPY --from=builder /app/dist ./dist 33 | COPY --from=builder /app/package*.json ./ 34 | COPY --from=builder /app/scripts ./scripts 35 | COPY --from=builder /app/server ./server 36 | COPY --from=builder /app/shared ./shared 37 | 38 | ENV HUSKY=0 39 | 40 | RUN npm ci --omit=dev 41 | 42 | EXPOSE 2308 43 | 44 | CMD ["npm", "run", "docker"] -------------------------------------------------------------------------------- /app/components/home/menu/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/breakpoints.scss' as *; 2 | @use '../../../styles/pxToRem.scss' as *; 3 | 4 | .home-menu { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | gap: pxToRem(20); 9 | margin: pxToRem(10) pxToRem(10) 0px pxToRem(10); 10 | } 11 | 12 | .home-menu-buttons { 13 | display: flex; 14 | gap: 10px; 15 | } 16 | 17 | .layout-option { 18 | display: flex; 19 | color: var(--white); 20 | font-size: var(--font-md); 21 | font-weight: 600; 22 | gap: pxToRem(10); 23 | align-items: center; 24 | } 25 | 26 | .home-menu-buttons-mobile { 27 | display: none; 28 | } 29 | 30 | @include mobile { 31 | .home-menu-buttons { 32 | display: none; 33 | } 34 | 35 | .home-menu-buttons-mobile { 36 | display: flex; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/home/monitor/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as MonitorCard } from './layout/card'; 2 | export { default as MonitorList } from './layout/list'; 3 | export { default as MonitorCompact } from './layout/compact'; 4 | -------------------------------------------------------------------------------- /app/components/home/monitor/layout/table.jsx: -------------------------------------------------------------------------------- 1 | import './table.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | const MonitorTable = ({ children }) => { 7 | return ( 8 |
9 |
10 |
Name
11 |
Ping
12 |
Uptime
13 |
Status
14 |
15 |
{children}
16 |
17 | ); 18 | }; 19 | 20 | MonitorTable.displayName = 'MonitorTable'; 21 | 22 | MonitorTable.propTypes = { 23 | children: PropTypes.node, 24 | }; 25 | 26 | export default MonitorTable; 27 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/body.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import Textarea from '../../../ui/textarea'; 3 | 4 | const MonitorHttpBody = ({ inputs, errors, handleInput }) => { 5 | const parseBody = (body) => { 6 | try { 7 | if (typeof body === 'object') { 8 | return JSON.stringify(body, null, 2); 9 | } 10 | } catch { 11 | return ''; 12 | } 13 | }; 14 | 15 | return ( 16 | <> 17 | 18 |
19 | Add a request body to be sent with the request. Make sure to follow JSON 20 | key/value format. 21 |
22 | 23 | 31 | 32 | ); 33 | }; 34 | 35 | MonitorHttpBody.displayName = 'MonitorHttpBody'; 36 | 37 | MonitorHttpBody.propTypes = { 38 | inputs: PropTypes.object.isRequired, 39 | errors: PropTypes.object.isRequired, 40 | handleInput: PropTypes.func.isRequired, 41 | }; 42 | 43 | export default MonitorHttpBody; 44 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/headers.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import Textarea from '../../../ui/textarea'; 3 | 4 | const MonitorHttpHeaders = ({ inputs, errors, handleInput }) => { 5 | const parseHeaders = (body) => { 6 | try { 7 | if (typeof body === 'object') { 8 | return JSON.stringify(body, null, 2); 9 | } 10 | } catch { 11 | return ''; 12 | } 13 | }; 14 | 15 | return ( 16 | <> 17 | 18 |
19 | Add additional headers to be sent with the request. Make sure to follow 20 | JSON key/value format. 21 |
22 | 23 | 31 | 32 | ); 33 | }; 34 | 35 | MonitorHttpHeaders.displayName = 'MonitorHttpHeaders'; 36 | 37 | MonitorHttpHeaders.propTypes = { 38 | inputs: PropTypes.object.isRequired, 39 | errors: PropTypes.object.isRequired, 40 | handleInput: PropTypes.func.isRequired, 41 | }; 42 | 43 | export default MonitorHttpHeaders; 44 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/http/index.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import TextInput from '../../../../ui/input'; 6 | import MonitorHttpMethods from './methods'; 7 | 8 | const MonitorAddHttp = ({ inputs, errors, handleInput }) => { 9 | const handleMethodSelect = (method) => { 10 | handleInput('method', method); 11 | }; 12 | 13 | return ( 14 | <> 15 | { 20 | handleInput('url', event.target.value); 21 | }} 22 | error={errors.url} 23 | /> 24 | 28 | {errors.method && ( 29 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | MonitorAddHttp.displayName = 'MonitorAddHttp'; 38 | 39 | MonitorAddHttp.propTypes = { 40 | inputs: PropTypes.object.isRequired, 41 | errors: PropTypes.object.isRequired, 42 | handleInput: PropTypes.func.isRequired, 43 | }; 44 | 45 | export default MonitorAddHttp; 46 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/initial/index.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import TextInput from '../../../../ui/input'; 6 | import MonitorInitialDropdown from './type'; 7 | 8 | const MonitorInitialType = ({ inputs, errors, handleInput, isEdit }) => { 9 | return ( 10 | <> 11 | handleInput('name', e.target.value)} 16 | error={errors.name} 17 | /> 18 | 19 | {!isEdit && ( 20 | 25 | )} 26 | 27 | {isEdit && ( 28 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | MonitorInitialType.displayName = 'MonitorInitialType'; 39 | 40 | MonitorInitialType.propTypes = { 41 | inputs: PropTypes.object.isRequired, 42 | errors: PropTypes.object.isRequired, 43 | handleInput: PropTypes.func.isRequired, 44 | isEdit: PropTypes.bool, 45 | }; 46 | 47 | export default MonitorInitialType; 48 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/notification/index.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import MonitorNotificationList from './list'; 6 | import MonitorNotificationType from './type'; 7 | 8 | const MonitorNotificationPage = ({ inputs, errors, handleInput }) => { 9 | return ( 10 | <> 11 | 16 | 17 | 22 | 23 | ); 24 | }; 25 | 26 | MonitorNotificationPage.displayName = 'MonitorNotificationPage'; 27 | 28 | MonitorNotificationPage.propTypes = { 29 | inputs: PropTypes.object.isRequired, 30 | errors: PropTypes.object.isRequired, 31 | handleInput: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default MonitorNotificationPage; 35 | -------------------------------------------------------------------------------- /app/components/modal/monitor/pages/tcp.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import TextInput from '../../../ui/input'; 6 | 7 | const MonitorPageTcp = ({ inputs, errors, handleInput }) => { 8 | return ( 9 | <> 10 | handleInput('url', e.target.value)} 16 | /> 17 | 18 | handleInput('port', e.target.value)} 24 | /> 25 | 26 | ); 27 | }; 28 | 29 | MonitorPageTcp.displayName = 'MonitorPageTcp'; 30 | 31 | MonitorPageTcp.propTypes = { 32 | inputs: PropTypes.object.isRequired, 33 | errors: PropTypes.object.isRequired, 34 | handleInput: PropTypes.func.isRequired, 35 | }; 36 | 37 | export default MonitorPageTcp; 38 | -------------------------------------------------------------------------------- /app/components/modal/monitor/styles.scss: -------------------------------------------------------------------------------- 1 | .monitor-configure-container { 2 | min-height: 300px; 3 | width: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /app/components/modal/notification/dropdown/icon.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const NotificationIcon = ({ name, icon }) => { 4 | return ( 5 |
6 | 7 |
{name}
8 |
9 | ); 10 | }; 11 | 12 | NotificationIcon.displayName = 'NotificationIcon'; 13 | 14 | NotificationIcon.propTypes = { 15 | name: PropTypes.string.isRequired, 16 | icon: PropTypes.string.isRequired, 17 | }; 18 | 19 | export default NotificationIcon; 20 | -------------------------------------------------------------------------------- /app/components/modal/notification/platform/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as Discord } from './discord'; 2 | export { default as Slack } from './slack'; 3 | export { default as Telegram } from './telegram'; 4 | export { default as Webhook } from './webhook'; 5 | -------------------------------------------------------------------------------- /app/components/modal/notification/styles.scss: -------------------------------------------------------------------------------- 1 | .json-format { 2 | white-space: pre; 3 | font-family: Consolas, monospace; 4 | background-color: var(--accent-800); 5 | padding: 8px 12px; 6 | border-radius: 12px; 7 | white-space: pre-wrap; 8 | word-break: break-all; 9 | 10 | .string { 11 | color: #7bc36e; 12 | } 13 | .number { 14 | color: #d19a66; 15 | } 16 | .boolean, 17 | .null { 18 | color: #56b6c2; 19 | } 20 | .key { 21 | color: #e06c75; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/components/modal/settings/account/avatar.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/pxToRem.scss' as *; 2 | 3 | .settings-modal-avatar-container { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .settings-modal-avatar-image { 10 | width: pxToRem(100); 11 | height: pxToRem(100); 12 | border-radius: var(--radius-pill); 13 | } 14 | 15 | .settings-modal-avatars-container { 16 | display: flex; 17 | flex-wrap: wrap; 18 | gap: 8px; 19 | } 20 | 21 | .settings-modal-avatar-option { 22 | width: pxToRem(60); 23 | height: pxToRem(60); 24 | border: pxToRem(2) solid #00000000; 25 | 26 | &:hover { 27 | border: pxToRem(2) solid var(--primary-600); 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | .settings-modal-avatar-option-select { 33 | width: pxToRem(60); 34 | height: pxToRem(60); 35 | border: pxToRem(2) solid var(--primary-600); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/modal/settings/avatars.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/pxToRem.scss' as *; 2 | 3 | .modal-avatar-container { 4 | width: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | margin: pxToRem(20) 0px; 9 | } 10 | 11 | .modal-avatar-image { 12 | width: pxToRem(200); 13 | height: pxToRem(200); 14 | border-radius: 50%; 15 | } 16 | 17 | .modal-avatar-options-container { 18 | display: flex; 19 | gap: pxToRem(10); 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .modal-avatar-option { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | width: 75px; 29 | height: 45px; 30 | background-color: var(--accent-900); 31 | border-radius: 5px; 32 | border: 1px solid var(--accent-500); 33 | color: var(--font-color); 34 | cursor: pointer; 35 | user-select: none; 36 | } 37 | 38 | .modal-avatar-name { 39 | flex: 1; 40 | height: 45px; 41 | background-color: var(--accent-900); 42 | border-radius: 5px; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | border: 1px solid var(--accent-500); 47 | color: var(--primary-500); 48 | font-size: 1.2rem; 49 | font-weight: bold; 50 | } 51 | -------------------------------------------------------------------------------- /app/components/modal/settings/manage/permissions.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/pxToRem.scss' as *; 2 | 3 | .permissions-container { 4 | display: flex; 5 | flex-direction: column; 6 | border: 2px solid var(--accent-600); 7 | padding: 5px 8px; 8 | border-radius: 8px; 9 | background-color: var(--accent-700); 10 | box-shadow: var(--shadow-sm); 11 | cursor: pointer; 12 | transition: var(--transition-fast); 13 | } 14 | 15 | .permissions-container-active { 16 | border: 2px solid var(--green-500); 17 | } 18 | 19 | .permissions-title { 20 | font-size: 1.2rem; 21 | font-weight: 600; 22 | margin-bottom: 5px; 23 | color: var(--font-color); 24 | } 25 | 26 | .permissions-title-active { 27 | color: var(--green-600); 28 | } 29 | 30 | .permissions-description { 31 | font-size: pxToRem(14); 32 | color: var(--font-color); 33 | } 34 | 35 | .member-permissions-message-container { 36 | display: flex; 37 | flex-direction: column; 38 | gap: pxToRem(15); 39 | } 40 | -------------------------------------------------------------------------------- /app/components/modal/status/configure/add.scss: -------------------------------------------------------------------------------- 1 | // status-configure-modal-add > scma 2 | 3 | .scma-container { 4 | display: grid; 5 | grid-template-columns: 1fr 1fr 1fr; 6 | grid-gap: 10px; 7 | margin: 20px 0px 10px 0px; 8 | } 9 | 10 | .scma-item { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | border: 2px solid var(--accent-700); 15 | padding: 10px; 16 | cursor: pointer; 17 | flex-direction: column; 18 | border-radius: 12px; 19 | transition: all 0.2s ease-in-out; 20 | 21 | &:hover { 22 | transition: all 0.2s ease-in-out; 23 | border-color: var(--primary-700); 24 | 25 | .scma-item-icon { 26 | transition: all 0.2s ease-in-out; 27 | color: var(--primary-700); 28 | } 29 | } 30 | 31 | &.active { 32 | border-color: var(--primary-700); 33 | 34 | .scma-item-icon { 35 | color: var(--primary-700); 36 | } 37 | } 38 | } 39 | 40 | .scma-item-name { 41 | font-size: var(--font-md); 42 | color: var(--font-color); 43 | } 44 | -------------------------------------------------------------------------------- /app/components/modal/status/configure/reorder.scss: -------------------------------------------------------------------------------- 1 | // status-configure-modal-reorder > scmr 2 | .scmr-container { 3 | display: flex; 4 | flex-direction: column; 5 | gap: 12px; 6 | width: 100%; 7 | overflow-x: hidden; 8 | min-width: 450px; 9 | } 10 | 11 | .scmr-block { 12 | display: flex; 13 | border: 2px solid var(--accent-400); 14 | padding: 12px; 15 | border-radius: var(--radius-md); 16 | gap: 8px; 17 | align-items: center; 18 | 19 | > div { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/components/modal/status/index.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/modal/status/index.jsx -------------------------------------------------------------------------------- /app/components/monitor/menu.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/breakpoints.scss' as *; 2 | 3 | .monitor-view-menu-container { 4 | display: flex; 5 | gap: 10px; 6 | background-color: var(--accent-900); 7 | padding: 10px; 8 | border-radius: var(--radius-md); 9 | } 10 | 11 | .monitor-view-menu-content { 12 | display: flex; 13 | gap: 10px; 14 | align-items: center; 15 | } 16 | 17 | .monitor-view-menu-name { 18 | flex: 1; 19 | display: flex; 20 | font-size: var(--font-2xl); 21 | font-weight: bold; 22 | flex-direction: column; 23 | justify-content: center; 24 | 25 | & > .subtitle { 26 | font-size: var(--font-sm); 27 | font-weight: normal; 28 | color: var(--font-light-color); 29 | 30 | & > span { 31 | font-weight: bold; 32 | } 33 | 34 | & > a { 35 | color: var(--primary-600); 36 | } 37 | } 38 | } 39 | 40 | .monitor-view-menu-container .dropdown { 41 | display: none; 42 | } 43 | 44 | @include mobile { 45 | .monitor-view-menu-container { 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | .monitor-view-menu-container .button { 51 | display: none; 52 | } 53 | 54 | .monitor-view-menu-container .dropdown { 55 | display: block; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/components/monitor/status.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/breakpoints.scss' as *; 2 | @use '../../styles/pxToRem.scss' as *; 3 | 4 | .monitor-status-container { 5 | display: grid; 6 | grid-template-columns: 1fr 1fr 1fr 1fr; 7 | background-color: var(--accent-900); 8 | border-radius: pxToRem(16); 9 | padding: pxToRem(8) 0; 10 | color: var(--font-color); 11 | } 12 | 13 | .monitor-status-content { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .monitor-status-title { 21 | font-size: var(--font-3xl); 22 | font-weight: var(--weight-bold); 23 | text-align: center; 24 | } 25 | 26 | .monitor-status-subtitle { 27 | font-size: var(--font-xl); 28 | font-weight: var(--weight-medium); 29 | color: var(--accent-200); 30 | } 31 | 32 | .montior-status-text { 33 | font-size: var(--font-2xl); 34 | } 35 | 36 | @include tablet { 37 | .monitor-status-container { 38 | grid-template-columns: 1fr 1fr; 39 | } 40 | } 41 | 42 | @include mobile { 43 | .monitor-status-container { 44 | grid-template-columns: 1fr; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/components/monitor/uptime.jsx: -------------------------------------------------------------------------------- 1 | import './uptime.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | // import local files 7 | import { heartbeatPropType } from '../../../shared/utils/propTypes'; 8 | import UptimeInfo from './updateInfo'; 9 | 10 | const MonitorUptime = ({ heartbeats = [] }) => { 11 | const highestLatency = heartbeats.reduce((acc, curr) => { 12 | return Math.max(acc, curr.latency); 13 | }, 0); 14 | 15 | const heartbeatList = heartbeats.map((heartbeat) => ( 16 | 21 | )); 22 | 23 | return ( 24 |
25 |
26 |
Status
27 |
Time
28 |
Message
29 |
Latency
30 |
31 | {heartbeatList} 32 |
33 | ); 34 | }; 35 | 36 | MonitorUptime.displayName = 'MonitorUptime'; 37 | 38 | MonitorUptime.propTypes = { 39 | heartbeats: PropTypes.arrayOf(heartbeatPropType).isRequired, 40 | }; 41 | 42 | export default MonitorUptime; 43 | -------------------------------------------------------------------------------- /app/components/navigation/index.jsx: -------------------------------------------------------------------------------- 1 | // import styles 2 | import './index.scss'; 3 | 4 | // import dependencies 5 | import PropTypes from 'prop-types'; 6 | 7 | // import local files 8 | import TopNavigation from './top'; 9 | import LeftNavigation from './left'; 10 | 11 | const Navigation = ({ children, activeUrl = '/home' }) => { 12 | return ( 13 |
14 | 15 |
16 | 17 |
{children}
18 |
19 |
20 | ); 21 | }; 22 | 23 | Navigation.displayName = 'Navigation'; 24 | 25 | Navigation.propTypes = { 26 | children: PropTypes.node, 27 | activeUrl: PropTypes.string, 28 | }; 29 | 30 | export default Navigation; 31 | -------------------------------------------------------------------------------- /app/components/navigation/index.scss: -------------------------------------------------------------------------------- 1 | .navigation-container { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100vw; 5 | height: 100vh; 6 | overflow: hidden; 7 | } 8 | 9 | .navigation-content { 10 | display: flex; 11 | height: calc(100vh - 65px); 12 | } 13 | 14 | .content { 15 | display: flex; 16 | flex: 1; 17 | overflow: auto; 18 | } 19 | -------------------------------------------------------------------------------- /app/components/navigation/top.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | @use '../../styles/breakpoints.scss' as *; 3 | 4 | .top-navigation { 5 | display: flex; 6 | width: 100vw; 7 | background-color: var(--accent-900); 8 | padding: pxToRem(4) pxToRem(4) pxToRem(4) pxToRem(8); 9 | border-bottom: 2px solid var(--accent-700); 10 | } 11 | 12 | .top-navigation-logo-container { 13 | display: flex; 14 | transition: var(--transition-base); 15 | color: var(--font-color); 16 | user-select: none; 17 | height: 55px; 18 | justify-content: center; 19 | align-items: center; 20 | 21 | &:hover { 22 | cursor: pointer; 23 | color: var(--primary-500); 24 | } 25 | } 26 | 27 | .top-navigation-logo-text { 28 | font-size: var(--font-2xl); 29 | font-weight: var(--weight-bold); 30 | display: flex; 31 | align-items: center; 32 | margin-left: pxToRem(4); 33 | } 34 | 35 | .top-navigation-right-container { 36 | display: flex; 37 | flex: 1; 38 | justify-content: flex-end; 39 | align-items: center; 40 | padding-right: pxToRem(8); 41 | } 42 | 43 | @include mobile { 44 | .top-navigation-logo-text { 45 | display: none; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/components/notifications/layout/compact.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/notifications/layout/compact.jsx -------------------------------------------------------------------------------- /app/components/notifications/layout/compact.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/notifications/layout/compact.scss -------------------------------------------------------------------------------- /app/components/notifications/menu/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/breakpoints.scss' as *; 2 | @use '../../../styles/pxToRem.scss' as *; 3 | 4 | .home-menu { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | gap: pxToRem(20); 9 | margin: pxToRem(10) pxToRem(10) 0px pxToRem(10); 10 | } 11 | 12 | .home-menu-buttons { 13 | display: flex; 14 | gap: 10px; 15 | } 16 | 17 | .layout-option { 18 | display: flex; 19 | color: var(--white); 20 | font-size: var(--font-md); 21 | font-weight: 600; 22 | gap: pxToRem(10); 23 | align-items: center; 24 | } 25 | 26 | .home-menu-buttons-mobile { 27 | display: none; 28 | } 29 | 30 | @include mobile { 31 | .home-menu-buttons { 32 | display: none; 33 | } 34 | 35 | .home-menu-buttons-mobile { 36 | display: flex; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/settings/about.jsx: -------------------------------------------------------------------------------- 1 | import './about.scss'; 2 | 3 | const SetttingAbout = () => { 4 | // eslint-disable-next-line no-undef 5 | const version = __APP_VERSION__ || '0.6.0'; 6 | 7 | return ( 8 |
9 | 10 | 11 |
Lunalytics
12 |
Version {version}
13 | 25 |
26 | ); 27 | }; 28 | 29 | export default SetttingAbout; 30 | -------------------------------------------------------------------------------- /app/components/settings/about.scss: -------------------------------------------------------------------------------- 1 | .settings-about-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | flex: 1; 7 | } 8 | 9 | .settings-about-title { 10 | font-size: var(--font-4xl); 11 | font-weight: 600; 12 | color: var(--primary-600); 13 | } 14 | 15 | .settings-about-version { 16 | font-size: var(--font-lg); 17 | font-weight: 600; 18 | color: var(--gray-400); 19 | } 20 | 21 | .settings-about-link { 22 | font-size: var(--font-md); 23 | font-weight: 600; 24 | color: var(--accent-200); 25 | margin-top: pxToRem(20); 26 | display: block; 27 | &:hover { 28 | color: var(--primary-800); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/components/settings/account/item/desktop.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/pxToRem.scss' as *; 2 | 3 | .settings-account-item-vertical { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | margin-bottom: pxToRem(8); 8 | } 9 | 10 | .settings-account-item-buttons { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 10px; 14 | margin-top: 10px; 15 | width: fit-content; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/settings/account/item/mobile.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/breakpoints.scss' as *; 2 | 3 | .settings-account-mobile-item { 4 | display: flex; 5 | padding: 12px 8px; 6 | justify-content: center; 7 | align-items: center; 8 | border-bottom: 2px solid var(--accent-700); 9 | user-select: none; 10 | 11 | &:hover { 12 | cursor: pointer; 13 | background-color: var(--accent-700); 14 | border-radius: var(--radius-md); 15 | } 16 | } 17 | 18 | .settings-account-mobile-item-title { 19 | font-size: var(--font-lg); 20 | font-weight: var(--weight-semibold); 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | .settings-account-mobile-item:last-child { 27 | border-bottom: none; 28 | } 29 | 30 | .settings-account-mobile-item-description { 31 | display: flex; 32 | flex: 1; 33 | justify-content: flex-end; 34 | align-items: center; 35 | gap: 8px; 36 | } 37 | 38 | .settings-account-mobile-item-icon { 39 | display: flex; 40 | color: var(--accent-50); 41 | } 42 | 43 | @include mobile { 44 | .settings-account-mobile-item-icon { 45 | width: 25px; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/components/settings/manage/index.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import { useEffect } from 'react'; 5 | import { toast } from 'react-toastify'; 6 | import { observer } from 'mobx-react-lite'; 7 | 8 | // import local files 9 | import { createGetRequest } from '../../../services/axios'; 10 | import MembersTable from './member'; 11 | import useTeamContext from '../../../context/team'; 12 | 13 | const ManageTeam = () => { 14 | const { teamMembers, setTeam } = useTeamContext(); 15 | 16 | const sortedMembers = teamMembers 17 | ?.sort((a, b) => a?.permission - b?.permission) 18 | .sort((a, b) => b?.isVerified - a?.isVerified); 19 | 20 | useEffect(() => { 21 | const fetchTeam = async () => { 22 | try { 23 | const query = await createGetRequest('/api/user/team'); 24 | 25 | setTeam(query.data); 26 | } catch { 27 | toast.error("Couldn't fetch team members"); 28 | } 29 | }; 30 | 31 | fetchTeam(); 32 | }, []); 33 | 34 | return ( 35 |
42 | 43 |
44 | ); 45 | }; 46 | 47 | ManageTeam.displayName = 'ManageTeam'; 48 | 49 | export default observer(ManageTeam); 50 | -------------------------------------------------------------------------------- /app/components/settings/manage/member/index.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | // import local files 7 | import MemberTableRow from './row'; 8 | import { userPropType } from '../../../../../shared/utils/propTypes'; 9 | 10 | const MembersTable = ({ members = [] }) => { 11 | const membersList = members.map((member, index) => ( 12 | 13 | )); 14 | 15 | return ( 16 |
17 |
18 |
Name
19 |
Joined
20 |
Permission
21 |
22 |
23 | 24 | {membersList} 25 |
26 | ); 27 | }; 28 | 29 | MembersTable.displayName = 'MembersTable'; 30 | 31 | MembersTable.propTypes = { 32 | members: PropTypes.arrayOf(userPropType).isRequired, 33 | }; 34 | 35 | export default MembersTable; 36 | -------------------------------------------------------------------------------- /app/components/settings/manage/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/settings/manage/style.scss -------------------------------------------------------------------------------- /app/components/settings/ui/menu/desktop.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import SettingsTab from '../tab/desktop'; 6 | import SettingsAccount from '../../account'; 7 | import SettingsPersonalisation from '../../personalisation'; 8 | import ManageTeam from '../../manage'; 9 | import SettingsAbout from '../../about'; 10 | import { IoMdClose } from '../../../icons'; 11 | 12 | const SettingsDesktop = ({ tab, handleTabUpdate, handleKeydown }) => { 13 | return ( 14 | <> 15 |
handleKeydown(null, true)}> 16 | 17 |
18 | 19 | {tab === 'Account' && } 20 | {tab === 'Appearance' && } 21 | {tab === 'Manage Team' && } 22 | {tab === 'About' && } 23 | 24 | ); 25 | }; 26 | 27 | SettingsDesktop.displayName = 'SettingsDesktop'; 28 | 29 | SettingsDesktop.propTypes = { 30 | tab: PropTypes.string.isRequired, 31 | handleTabUpdate: PropTypes.func.isRequired, 32 | handleKeydown: PropTypes.func.isRequired, 33 | }; 34 | 35 | export default SettingsDesktop; 36 | -------------------------------------------------------------------------------- /app/components/settings/ui/tab/desktop.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | const tabs = [ 5 | { title: 'GENERAL', items: ['Account', 'Appearance'] }, 6 | { title: 'WORKSPACE', items: ['Manage Team', 'About'] }, 7 | ]; 8 | 9 | const SettingsTab = ({ tab, handleTabUpdate }, index) => { 10 | const tabsList = tabs.map(({ title, items }) => { 11 | const itemsList = items.map((name) => { 12 | const active = name === tab; 13 | return ( 14 |
handleTabUpdate(name)} 18 | id={name.replace(' ', '-')} 19 | > 20 | {name} 21 |
22 | ); 23 | }); 24 | 25 | return ( 26 |
27 | {title &&
{title}
} 28 |
{itemsList}
29 |
30 | ); 31 | }); 32 | 33 | return
{tabsList}
; 34 | }; 35 | 36 | SettingsTab.displayName = 'SettingsTab'; 37 | 38 | SettingsTab.propTypes = { 39 | tab: PropTypes.string.isRequired, 40 | handleTabUpdate: PropTypes.func.isRequired, 41 | }; 42 | 43 | export default SettingsTab; 44 | -------------------------------------------------------------------------------- /app/components/setup/style.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .auth-setup-back-button { 4 | position: absolute; 5 | top: pxToRem(10); 6 | left: pxToRem(10); 7 | font-size: var(--font-xl); 8 | cursor: pointer; 9 | display: flex; 10 | align-items: center; 11 | gap: pxToRem(5); 12 | } 13 | 14 | .auth-setup-type-container { 15 | display: flex; 16 | gap: pxToRem(15); 17 | flex-direction: column; 18 | margin: pxToRem(32) 0; 19 | } 20 | 21 | .auth-setup-type-content { 22 | flex: 1; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | padding: pxToRem(12) pxToRem(16); 27 | border: pxToRem(1) solid var(--accent-600); 28 | background-color: var(--accent-800); 29 | cursor: pointer; 30 | color: var(--font-color); 31 | border-radius: pxToRem(12); 32 | flex-direction: column; 33 | } 34 | 35 | .auth-setup-type-title { 36 | font-size: var(--font-xl); 37 | font-weight: bold; 38 | } 39 | 40 | .auth-setup-type-subtitle { 41 | text-align: center; 42 | color: var(--font-light-color); 43 | font-size: var(--font-sm); 44 | } 45 | -------------------------------------------------------------------------------- /app/components/status/configure/appearance/branding.jsx: -------------------------------------------------------------------------------- 1 | // import local files 2 | import useStatusContext from '../../../../hooks/useConfigureStatus'; 3 | import TextInput from '../../../ui/input'; 4 | 5 | const StatusConfigureAppearanceBranding = () => { 6 | const { 7 | settings: { logo, favicon }, 8 | changeValues, 9 | } = useStatusContext(); 10 | 11 | return ( 12 |
13 |
14 | changeValues({ logo: e.target.value })} 19 | /> 20 |
21 |
22 | changeValues({ favicon: e.target.value })} 27 | /> 28 |
29 |
30 | ); 31 | }; 32 | 33 | StatusConfigureAppearanceBranding.displayName = 34 | 'StatusConfigureAppearanceBranding'; 35 | 36 | StatusConfigureAppearanceBranding.propTypes = {}; 37 | 38 | export default StatusConfigureAppearanceBranding; 39 | -------------------------------------------------------------------------------- /app/components/status/configure/appearance/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/pxToRem.scss' as *; 2 | 3 | // status-configure-content > scc 4 | 5 | .scc-block { 6 | display: flex; 7 | flex-direction: column; 8 | background-color: var(--accent-900); 9 | padding: pxToRem(8) pxToRem(12); 10 | border-radius: var(--radius-md); 11 | box-shadow: var(--shadow-md); 12 | margin-bottom: pxToRem(48); 13 | gap: pxToRem(12); 14 | position: relative; 15 | padding-bottom: pxToRem(25); 16 | } 17 | 18 | .scc-title { 19 | font-size: var(--font-xl); 20 | font-weight: 700; 21 | } 22 | 23 | .scc-description { 24 | font-size: var(--font-sm); 25 | color: var(--font-light-color); 26 | } 27 | 28 | // status-configure-colors > scc 29 | .scc-container { 30 | display: grid; 31 | grid-template-columns: repeat(4, 1fr); 32 | gap: pxToRem(16); 33 | } 34 | 35 | .scc-container > div { 36 | flex: 1; 37 | z-index: 1; 38 | } 39 | 40 | // status-configure-branding > scb 41 | 42 | .scb-container { 43 | display: grid; 44 | grid-template-columns: repeat(2, 1fr); 45 | gap: pxToRem(16); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/history/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../styles/pxToRem.scss' as *; 2 | @use '../../../../../styles/breakpoints.scss' as *; 3 | 4 | // sclh > status-configure-layout-history 5 | .sclh-container { 6 | display: flex; 7 | flex-direction: column; 8 | gap: 12px; 9 | margin-bottom: 16px; 10 | flex: 1; 11 | width: 100%; 12 | max-width: var(--status-page-max-width); 13 | } 14 | 15 | .sclh-title { 16 | font-size: var(--font-lg); 17 | font-weight: 600; 18 | } 19 | 20 | .sclh-date { 21 | display: flex; 22 | flex-direction: row; 23 | gap: 16px; 24 | margin: 16px 0 8px 0; 25 | border-bottom: 2px solid var(--accent-700); 26 | padding-bottom: 8px; 27 | font-size: var(--font-lg); 28 | } 29 | 30 | .sclh-subtitle { 31 | color: var(--font-light-color); 32 | font-size: var(--font-sm); 33 | } 34 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/incidents/design/basic.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import dayjs from 'dayjs'; 3 | import PropTypes from 'prop-types'; 4 | import classNames from 'classnames'; 5 | 6 | const StatusIncidentBasic = ({ incidents = [], status }) => { 7 | const incidentsList = !incidents?.length ? [] : incidents; 8 | 9 | const containerClasses = classNames('sci-content', status); 10 | 11 | return ( 12 |
13 |
14 | {incidentsList.map((incident, index) => ( 15 |
16 |
17 | {incident.title} - {incident.description} 18 |
19 |
20 | {dayjs(incident.timestamp).format('MMM DD YYYY, HH:mm')} 21 |
22 |
23 | ))} 24 |
25 |
26 | ); 27 | }; 28 | 29 | StatusIncidentBasic.displayName = 'StatusIncidentBasic'; 30 | 31 | StatusIncidentBasic.propTypes = { 32 | incidents: PropTypes.array.isRequired, 33 | status: PropTypes.string.isRequired, 34 | }; 35 | 36 | export default StatusIncidentBasic; 37 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/metrics/area.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/status/configure/layout/metrics/area.jsx -------------------------------------------------------------------------------- /app/components/status/configure/layout/metrics/dropdown.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import useStatusContext from '../../../../../hooks/useConfigureStatus'; 6 | import StatusConfigureLayoutMetricsTypeBasic from './type/basic'; 7 | 8 | const StatusConfigureLayoutMetricsDropdown = ({ componentId, title }) => { 9 | const { getComponent } = useStatusContext(); 10 | 11 | const { data: { showName, showPing } = {} } = getComponent(componentId); 12 | 13 | return ( 14 | <> 15 |
Graph options
16 |
17 | 21 |
22 | 23 | ); 24 | }; 25 | 26 | StatusConfigureLayoutMetricsDropdown.displayName = 27 | 'StatusConfigureLayoutMetricsDropdown'; 28 | 29 | StatusConfigureLayoutMetricsDropdown.propTypes = { 30 | componentId: PropTypes.string.isRequired, 31 | title: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default StatusConfigureLayoutMetricsDropdown; 35 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/metrics/type/basic.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import StatusLayoutLineChart from './chart/line'; 6 | 7 | const StatusConfigureLayoutMetricsTypeBasic = ({ showPing, showTitle }) => { 8 | return ( 9 |
10 |
11 | {showTitle || showPing ? ( 12 |
13 |
{showTitle ? showTitle : null}
14 | {showPing &&
30 ms
} 15 |
16 | ) : null} 17 |
18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | StatusConfigureLayoutMetricsTypeBasic.displayName = 26 | 'StatusConfigureLayoutMetricsTypeBasic'; 27 | 28 | StatusConfigureLayoutMetricsTypeBasic.propTypes = { 29 | showPing: PropTypes.bool.isRequired, 30 | showTitle: PropTypes.string.isRequired, 31 | }; 32 | 33 | export default StatusConfigureLayoutMetricsTypeBasic; 34 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/metrics/type/styles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/components/status/configure/layout/metrics/type/styles.scss -------------------------------------------------------------------------------- /app/components/status/configure/layout/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/pxToRem.scss' as *; 2 | 3 | // status-configure-layout > scl 4 | 5 | .scl-menu { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | position: absolute; 10 | top: pxToRem(10); 11 | right: pxToRem(10); 12 | gap: pxToRem(12); 13 | } 14 | 15 | .scl-bin { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | transition: all 0.25s ease-in-out; 20 | 21 | &:hover { 22 | cursor: pointer; 23 | color: var(--red-700); 24 | transition: all 0.25s ease-in-out; 25 | } 26 | } 27 | 28 | .scl-minimize { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | transition: all 0.25s ease-in-out; 33 | 34 | &:hover { 35 | cursor: pointer; 36 | color: var(--accent-200); 37 | transition: all 0.25s ease-in-out; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/uptime/graph/basic.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { 6 | FaCircleCheck, 7 | FaClock, 8 | IoWarning, 9 | RiIndeterminateCircleFill, 10 | } from '../../../../../icons'; 11 | 12 | const statusAndText = { 13 | Icon: { 14 | Operational: , 15 | Maintenance: , 16 | Incident: , 17 | Outage: , 18 | }, 19 | Text: { 20 | Operational: 'Operational', 21 | Maintenance: 'Maintenance', 22 | Incident: 'Incident', 23 | Outage: 'Outage', 24 | }, 25 | }; 26 | 27 | const StatusUptimeBasicGraph = ({ monitor = {}, indicator }) => { 28 | const iconOrText = 29 | statusAndText[indicator][monitor.status] || 30 | statusAndText[indicator].Operational; 31 | 32 | return ( 33 |
34 |
{monitor.name || 'Monitor'}
35 |
{iconOrText}
36 |
37 | ); 38 | }; 39 | 40 | StatusUptimeBasicGraph.displayName = 'StatusUptimeBasicGraph'; 41 | 42 | StatusUptimeBasicGraph.propTypes = { 43 | monitor: PropTypes.object.isRequired, 44 | indicator: PropTypes.string.isRequired, 45 | }; 46 | 47 | export default StatusUptimeBasicGraph; 48 | -------------------------------------------------------------------------------- /app/components/status/configure/layout/uptime/graph/index.jsx: -------------------------------------------------------------------------------- 1 | import './styles.scss'; 2 | 3 | export { default as StatusUptimeBasicGraph } from './basic'; 4 | export { default as StatusUptimePrettyGraph } from './pretty'; 5 | export { default as StatusUptimeNerdyGraph } from './nerdy'; 6 | -------------------------------------------------------------------------------- /app/components/status/configure/monitor/item.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { FaTrashCan } from '../../../icons'; 6 | 7 | const StatusConfigureMonitorItem = ({ monitor, removeMonitor }) => { 8 | if (!monitor) return null; 9 | 10 | return ( 11 |
12 |
13 |
{monitor.name}
14 |
{monitor.url}
15 |
16 |
removeMonitor(monitor.monitorId)} 19 | > 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | StatusConfigureMonitorItem.displayName = 'StatusConfigureMonitorItem'; 27 | 28 | StatusConfigureMonitorItem.propTypes = { 29 | monitor: PropTypes.object, 30 | removeMonitor: PropTypes.func, 31 | }; 32 | 33 | export default StatusConfigureMonitorItem; 34 | -------------------------------------------------------------------------------- /app/components/status/configure/preview/footer.jsx: -------------------------------------------------------------------------------- 1 | const StatusFooter = () => { 2 | return ( 3 | 12 | ); 13 | }; 14 | 15 | export default StatusFooter; 16 | -------------------------------------------------------------------------------- /app/components/status/configure/preview/header/status.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import dayjs from 'dayjs'; 3 | import classNames from 'classnames'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const StatusPageHeaderStatus = ({ status = {} }) => { 7 | if (!status.showLogo && !status.showTitle) { 8 | return null; 9 | } 10 | 11 | const containerClasses = classNames('spht-status-container', { 12 | [status.alignment]: true, 13 | [`position-${status.position}`]: true, 14 | }); 15 | 16 | const titleClasses = classNames('spht-status-title', { 17 | [status.titleSize]: status.showTitle, 18 | }); 19 | 20 | const subtitleClasses = classNames('spht-status-subtitle', { 21 | [status.statusSize]: status.showStatus, 22 | }); 23 | 24 | return ( 25 | 26 | {status.showStatus &&
Service Status
} 27 | {status.showTitle && ( 28 |
29 | Last check: {dayjs().format('HH:mm:ss')} 30 |
31 | )} 32 |
33 | ); 34 | }; 35 | 36 | StatusPageHeaderStatus.displayName = 'StatusPageHeaderStatus'; 37 | 38 | StatusPageHeaderStatus.propTypes = { 39 | status: PropTypes.object.isRequired, 40 | }; 41 | 42 | export default StatusPageHeaderStatus; 43 | -------------------------------------------------------------------------------- /app/components/status/configure/preview/metrics/graph/basic.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import StatusLayoutLineChart from '../../../layout/metrics/type/chart/line'; 6 | 7 | const StatusPageMetricsBasicGraph = ({ title, showPing, heartbeats = [] }) => { 8 | if (!heartbeats.length) return null; 9 | 10 | const lastHeartbeat = heartbeats[heartbeats.length - 1]; 11 | const ms = lastHeartbeat.latency; 12 | 13 | return ( 14 |
15 |
16 |
{title}
17 | {showPing && ( 18 |
{ms.toLocaleString()} ms
19 | )} 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | StatusPageMetricsBasicGraph.displayName = 'StatusPageMetricsBasicGraph'; 29 | 30 | StatusPageMetricsBasicGraph.propTypes = { 31 | title: PropTypes.string.isRequired, 32 | showPing: PropTypes.bool.isRequired, 33 | heartbeats: PropTypes.array.isRequired, 34 | }; 35 | 36 | export default StatusPageMetricsBasicGraph; 37 | -------------------------------------------------------------------------------- /app/components/ui/accordion/accordion.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { useEffect, useState } from 'react'; 3 | import { AccordionContext } from './context'; 4 | 5 | const Accordion = ({ 6 | children, 7 | value = [], 8 | onChange, 9 | showMultiple = false, 10 | dark = false, 11 | ...props 12 | }) => { 13 | const [selected, setSelected] = useState(value); 14 | 15 | useEffect(() => { 16 | onChange?.(selected); 17 | }, [selected, onChange]); 18 | 19 | return ( 20 |
    21 | 24 | {children} 25 | 26 |
27 | ); 28 | }; 29 | 30 | Accordion.displayName = 'Accordion'; 31 | 32 | Accordion.propTypes = { 33 | children: PropTypes.node, 34 | value: PropTypes.array, 35 | onChange: PropTypes.func, 36 | showMultiple: PropTypes.bool, 37 | dark: PropTypes.bool, 38 | }; 39 | 40 | export default Accordion; 41 | -------------------------------------------------------------------------------- /app/components/ui/accordion/accordion.scss: -------------------------------------------------------------------------------- 1 | .accordion-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5rem; 5 | } 6 | 7 | .accordion-list-item, 8 | .accordion-list-item-dark { 9 | background-color: var(--accent-800); 10 | border: 2px solid var(--accent-700); 11 | padding: 0.5rem; 12 | border-radius: var(--radius-md); 13 | } 14 | 15 | .accordion-list-item-dark { 16 | background-color: var(--accent-900); 17 | } 18 | 19 | .accordion-list-item header { 20 | display: flex; 21 | justify-content: space-between; 22 | font-weight: bold; 23 | cursor: pointer; 24 | transition: all 0.3s ease-in-out; 25 | align-items: center; 26 | gap: 1rem; 27 | 28 | &:hover { 29 | .accordion-list-item-title { 30 | color: var(--primary-500); 31 | } 32 | } 33 | } 34 | 35 | .accordion-list-item-title { 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | transition: all 0.3s ease-in-out; 40 | } 41 | 42 | .accordion-list-item-tag { 43 | background-color: var(--accent-700); 44 | padding: 0.25rem 0.5rem; 45 | border-radius: var(--radius-sm); 46 | font-size: 0.75rem; 47 | color: var(--font-color); 48 | border: 2px solid var(--accent-600); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/ui/accordion/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const AccordionContext = createContext(); 4 | -------------------------------------------------------------------------------- /app/components/ui/accordion/index.jsx: -------------------------------------------------------------------------------- 1 | import './accordion.scss'; 2 | 3 | export { default as Accordion } from './accordion'; 4 | export { default as AccordionItem } from './item'; 5 | -------------------------------------------------------------------------------- /app/components/ui/alert/error.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { MdErrorOutline } from '../../icons'; 6 | 7 | const AlertError = ({ title = 'Error', description }) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
{title}
15 | {description &&
{description}
} 16 |
17 |
18 | ); 19 | }; 20 | 21 | AlertError.displayName = 'AlertError'; 22 | 23 | AlertError.propTypes = { 24 | title: PropTypes.string, 25 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 26 | }; 27 | 28 | export default AlertError; 29 | -------------------------------------------------------------------------------- /app/components/ui/alert/index.jsx: -------------------------------------------------------------------------------- 1 | import './alert.scss'; 2 | 3 | // import local files 4 | import AlertError from './error'; 5 | import AlertInfo from './info'; 6 | import AlertSuccess from './success'; 7 | import AlertWarning from './warning'; 8 | 9 | const Alert = { 10 | Error: AlertError, 11 | Info: AlertInfo, 12 | Success: AlertSuccess, 13 | Warning: AlertWarning, 14 | }; 15 | 16 | export { AlertError, AlertInfo, AlertSuccess, AlertWarning }; 17 | 18 | export default Alert; 19 | -------------------------------------------------------------------------------- /app/components/ui/alert/info.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { MdErrorOutline } from '../../icons'; 6 | 7 | const AlertInfo = ({ title = 'Info', description }) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
{title}
15 | {description &&
{description}
} 16 |
17 |
18 | ); 19 | }; 20 | 21 | AlertInfo.displayName = 'AlertInfo'; 22 | 23 | AlertInfo.propTypes = { 24 | title: PropTypes.string, 25 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 26 | }; 27 | 28 | export default AlertInfo; 29 | -------------------------------------------------------------------------------- /app/components/ui/alert/success.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { MdErrorOutline } from '../../icons'; 6 | 7 | const AlertSuccess = ({ title = 'Success', description }) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
{title}
15 | {description &&
{description}
} 16 |
17 |
18 | ); 19 | }; 20 | 21 | AlertSuccess.displayName = 'AlertSuccess'; 22 | 23 | AlertSuccess.propTypes = { 24 | title: PropTypes.string, 25 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 26 | }; 27 | 28 | export default AlertSuccess; 29 | -------------------------------------------------------------------------------- /app/components/ui/alert/warning.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { MdErrorOutline } from '../../icons'; 6 | 7 | const AlertWarning = ({ title = 'Warning', description }) => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
{title}
15 | {description &&
{description}
} 16 |
17 |
18 | ); 19 | }; 20 | 21 | AlertWarning.displayName = 'AlertWarning'; 22 | 23 | AlertWarning.propTypes = { 24 | title: PropTypes.string, 25 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 26 | }; 27 | 28 | export default AlertWarning; 29 | -------------------------------------------------------------------------------- /app/components/ui/avatar.jsx: -------------------------------------------------------------------------------- 1 | // import styles 2 | import './avatar.scss'; 3 | 4 | // import dependencies 5 | import PropTypes from 'prop-types'; 6 | import { observer } from 'mobx-react-lite'; 7 | 8 | // import local files 9 | import useContextStore from '../../context'; 10 | 11 | const isImageUrl = (url) => { 12 | if (typeof url !== 'string') { 13 | return false; 14 | } 15 | return url.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/gim) !== null; 16 | }; 17 | 18 | const Avatar = ({ showUsername = true, showAvatar = true }) => { 19 | const { 20 | userStore: { 21 | user: { avatar, displayName }, 22 | }, 23 | } = useContextStore(); 24 | 25 | const avatarUrl = isImageUrl(avatar) ? avatar : `/icons/${avatar}.png`; 26 | 27 | const userAvatar = avatar ? ( 28 | 29 | ) : ( 30 |
{displayName?.charAt(0)}
31 | ); 32 | 33 | return ( 34 |
35 | {showAvatar && userAvatar} 36 | {showUsername &&
{displayName}
} 37 |
38 | ); 39 | }; 40 | 41 | Avatar.displayName = 'Avatar'; 42 | 43 | Avatar.propTypes = { 44 | showUsername: PropTypes.bool, 45 | showAvatar: PropTypes.bool, 46 | }; 47 | 48 | export default observer(Avatar); 49 | -------------------------------------------------------------------------------- /app/components/ui/avatar.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .avatar-container { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | color: var(--font-color); 8 | 9 | &:hover { 10 | cursor: pointer; 11 | color: var(--primary-500); 12 | } 13 | 14 | .avatar-default:hover { 15 | color: var(--font-color); 16 | } 17 | } 18 | 19 | .avatar { 20 | width: pxToRem(40); 21 | height: pxToRem(40); 22 | border-radius: var(--radius-pill); 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | margin-right: pxToRem(6); 27 | } 28 | 29 | .avatar-default { 30 | width: pxToRem(40); 31 | height: pxToRem(40); 32 | border-radius: var(--radius-pill); 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | margin-right: pxToRem(6); 37 | background-color: var(--primary-700); 38 | font-size: var(--font-xl); 39 | color: var(--font-color); 40 | } 41 | 42 | .avatar-username { 43 | font-size: var(--font-xl); 44 | font-weight: bold; 45 | transition: var(--transition-base); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/ui/button.jsx: -------------------------------------------------------------------------------- 1 | import './button.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | // import local files 8 | import { colorPropType } from '../../../shared/utils/propTypes'; 9 | 10 | const Button = ({ 11 | children, 12 | iconLeft, 13 | iconRight, 14 | color, 15 | outline, 16 | fullWidth, 17 | tabIndex = 0, 18 | as: Wrapper = 'div', 19 | ...props 20 | }) => { 21 | const classes = classNames('button', { 22 | [`button--${color}`]: !outline && color, 23 | [`button--${outline}-outline`]: !color && outline, 24 | 'button-fixed-width': !fullWidth, 25 | }); 26 | 27 | return ( 28 | 29 | {iconLeft} 30 | {children &&
{children}
} 31 | {iconRight} 32 |
33 | ); 34 | }; 35 | 36 | Button.displayName = 'Button'; 37 | 38 | Button.propTypes = { 39 | children: PropTypes.node, 40 | iconLeft: PropTypes.node, 41 | iconRight: PropTypes.node, 42 | color: colorPropType, 43 | outline: colorPropType, 44 | fullWidth: PropTypes.bool, 45 | tabIndex: PropTypes.number, 46 | as: PropTypes.elementType, 47 | }; 48 | 49 | export default Button; 50 | -------------------------------------------------------------------------------- /app/components/ui/checkbox.jsx: -------------------------------------------------------------------------------- 1 | import './checkbox.scss'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Checkbox = ({ label, description, shortDescription, ...props }) => { 5 | return ( 6 | <> 7 |
8 |
9 | 10 | {shortDescription && ( 11 |
{shortDescription}
12 | )} 13 |
14 | 18 |
19 | 20 | {description &&
{description}
} 21 | 22 | ); 23 | }; 24 | 25 | Checkbox.displayName = 'Checkbox'; 26 | 27 | Checkbox.propTypes = { 28 | label: PropTypes.string, 29 | description: PropTypes.string, 30 | shortDescription: PropTypes.string, 31 | }; 32 | 33 | export default Checkbox; 34 | -------------------------------------------------------------------------------- /app/components/ui/dropdown/index.jsx: -------------------------------------------------------------------------------- 1 | // import scss 2 | import './dropdown.scss'; 3 | 4 | // import components 5 | import Container from './container'; 6 | import Item from './item'; 7 | import List from './list'; 8 | import Trigger from './trigger'; 9 | 10 | const Dropdown = { 11 | Container, 12 | Item, 13 | List, 14 | Trigger, 15 | }; 16 | 17 | export { Container, Item, List, Trigger }; 18 | 19 | export default Dropdown; 20 | -------------------------------------------------------------------------------- /app/components/ui/dropdown/item.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import classNames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // import local files 6 | import { colorPropType } from '../../../../shared/utils/propTypes'; 7 | 8 | const Item = ({ 9 | dotColor, 10 | showDot = false, 11 | isSelected = false, 12 | children, 13 | as: Wrapper = 'div', 14 | ...props 15 | }) => { 16 | const classes = classNames('dropdown-item-dot', { 17 | 'dropdown-item-selected': isSelected, 18 | [`dropdown-item-dot--${dotColor}`]: dotColor, 19 | }); 20 | 21 | return ( 22 | 23 | {showDot && } 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | Item.displayName = 'Dropdown.Item'; 30 | 31 | Item.propTypes = { 32 | dotColor: colorPropType, 33 | showDot: PropTypes.bool, 34 | isSelected: PropTypes.bool, 35 | children: PropTypes.node, 36 | as: PropTypes.elementType, 37 | }; 38 | 39 | export default Item; 40 | -------------------------------------------------------------------------------- /app/components/ui/dropdown/list.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | const List = ({ fullWidth, isOpen, children, ...props }) => { 6 | const classes = classNames('dropdown-body', { 7 | 'dropdown-list-full-width': fullWidth, 8 | 'dropdown-body-open': isOpen, 9 | }); 10 | 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | List.displayName = 'Dropdown.List'; 19 | 20 | List.propTypes = { 21 | fullWidth: PropTypes.bool, 22 | isOpen: PropTypes.bool, 23 | children: PropTypes.node, 24 | }; 25 | 26 | export default List; 27 | -------------------------------------------------------------------------------- /app/components/ui/input.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .text-input-container { 4 | position: relative; 5 | } 6 | 7 | .text-input { 8 | width: 100%; 9 | height: pxToRem(50); 10 | background-color: var(--accent-800); 11 | border: none; 12 | outline: none; 13 | color: var(--font-color); 14 | padding: 0 pxToRem(8); 15 | font-size: var(--font-md); 16 | font-weight: var(--weight-medium); 17 | border-radius: var(--radius-md); 18 | margin-top: pxToRem(4); 19 | border-color: var(--primary-700); 20 | border: 2px solid #ffffff00; 21 | 22 | &:focus { 23 | border: 2px solid var(--primary-700); 24 | } 25 | } 26 | 27 | .text-input-icon-left { 28 | padding-left: pxToRem(32); 29 | } 30 | 31 | .text-input-icon-right { 32 | padding-right: pxToRem(32); 33 | } 34 | 35 | .text-left-icon { 36 | position: absolute; 37 | box-sizing: border-box; 38 | left: 8px; 39 | top: 34px; 40 | transform: translateY(-50%); 41 | } 42 | 43 | .text-right-icon { 44 | position: absolute; 45 | box-sizing: border-box; 46 | right: 8px; 47 | top: 34px; 48 | transform: translateY(-50%); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/ui/modal/actions.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | const Actions = ({ children, ...props }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | Actions.displayName = 'Modal.Actions'; 15 | 16 | Actions.propTypes = { 17 | children: PropTypes.node, 18 | }; 19 | 20 | export default Actions; 21 | -------------------------------------------------------------------------------- /app/components/ui/modal/button.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | import { colorPropType } from '../../../../shared/utils/propTypes'; 7 | 8 | const Button = ({ children, color, ...props }) => { 9 | const classes = classNames('modal-button', { 10 | [`modal-button--${color}`]: color, 11 | }); 12 | 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | }; 19 | 20 | Button.displayName = 'Modal.Button'; 21 | 22 | Button.propTypes = { 23 | children: PropTypes.node, 24 | color: colorPropType, 25 | }; 26 | 27 | export default Button; 28 | -------------------------------------------------------------------------------- /app/components/ui/modal/container.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | // import local files 8 | import { IoMdClose } from '../../icons'; 9 | 10 | const Container = ({ 11 | children, 12 | glassmorph, 13 | closeButton, 14 | contentProps = {}, 15 | ...props 16 | }) => { 17 | const classes = classNames('modal-container', { 18 | 'modal-container--glassmorph': glassmorph, 19 | 'modal-container--no-glassmorph': !glassmorph, 20 | }); 21 | 22 | return ( 23 |
24 |
25 | {closeButton && ( 26 |
27 | 28 |
29 | )} 30 | {children} 31 |
32 |
33 | ); 34 | }; 35 | 36 | Container.displayName = 'Modal.Container'; 37 | 38 | Container.propTypes = { 39 | children: PropTypes.node, 40 | glassmorph: PropTypes.bool, 41 | closeButton: PropTypes.func, 42 | contentProps: PropTypes.object, 43 | }; 44 | 45 | export default Container; 46 | -------------------------------------------------------------------------------- /app/components/ui/modal/index.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import local files 4 | import Actions from './actions'; 5 | import Button from './button'; 6 | import Container from './container'; 7 | import Message from './message'; 8 | import Title from './title'; 9 | 10 | const Modal = { 11 | Actions, 12 | Button, 13 | Container, 14 | Message, 15 | Title, 16 | }; 17 | 18 | export default Modal; 19 | -------------------------------------------------------------------------------- /app/components/ui/modal/message.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | const Message = ({ children, ...props }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | Message.displayName = 'Modal.Message'; 15 | 16 | Message.propTypes = { 17 | children: PropTypes.node, 18 | }; 19 | 20 | export default Message; 21 | -------------------------------------------------------------------------------- /app/components/ui/modal/title.jsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | 6 | const Title = ({ children, ...props }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | Title.displayName = 'Modal.Title'; 15 | 16 | Title.propTypes = { 17 | children: PropTypes.node, 18 | }; 19 | 20 | export default Title; 21 | -------------------------------------------------------------------------------- /app/components/ui/progress.jsx: -------------------------------------------------------------------------------- 1 | import './progress.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | const ProgressBar = ({ sections = 1, progress = 1 }) => { 8 | // create an array of divs that match the number of sections 9 | 10 | const sectionsArray = Array.from({ length: sections }, (_, i) => { 11 | const classes = classNames({ 12 | 'progress-section': i + 1 > progress, 13 | 'progress-section-active': i + 1 === progress, 14 | 'progress-section-complete': i + 1 < progress, 15 | }); 16 | 17 | return
; 18 | }); 19 | 20 | return ( 21 |
29 | {sectionsArray} 30 |
31 | ); 32 | }; 33 | 34 | ProgressBar.displayName = 'ProgressBar'; 35 | 36 | ProgressBar.propTypes = { 37 | sections: PropTypes.number, 38 | progress: PropTypes.number, 39 | }; 40 | 41 | export default ProgressBar; 42 | -------------------------------------------------------------------------------- /app/components/ui/progress.scss: -------------------------------------------------------------------------------- 1 | .progress-section { 2 | width: 25px; 3 | max-width: 85px; 4 | height: 10px; 5 | border-radius: 8px; 6 | background-color: var(--accent-500); 7 | } 8 | 9 | .progress-section-active { 10 | flex: 1; 11 | max-width: 85px; 12 | height: 10px; 13 | border-radius: 8px; 14 | background-color: var(--primary-600); 15 | } 16 | 17 | .progress-section-complete { 18 | width: 25px; 19 | height: 10px; 20 | border-radius: 8px; 21 | background-color: var(--primary-600); 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui/searchBar.jsx: -------------------------------------------------------------------------------- 1 | // import styles 2 | import './searchBar.scss'; 3 | 4 | // import dependencies 5 | import PropTypes from 'prop-types'; 6 | 7 | const SearchBar = ({ label, id, tabIndex = 0, ...props }) => { 8 | return ( 9 | <> 10 | {label && } 11 | 18 | 19 | ); 20 | }; 21 | 22 | SearchBar.displayName = 'SearchBar'; 23 | 24 | SearchBar.propTypes = { 25 | label: PropTypes.string, 26 | id: PropTypes.string, 27 | tabIndex: PropTypes.number, 28 | }; 29 | 30 | export default SearchBar; 31 | -------------------------------------------------------------------------------- /app/components/ui/searchBar.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .search-bar-input { 4 | width: 100%; 5 | height: 100%; 6 | border: none; 7 | outline: none; 8 | color: var(--font-color); 9 | padding: 0 pxToRem(10); 10 | font-size: var(--font-md); 11 | font-weight: var(--weight-medium); 12 | border-radius: var(--radius-md); 13 | background-color: var(--accent-700); 14 | border-color: var(--primary-700); 15 | border: 2px solid #ffffff00; 16 | 17 | &:focus { 18 | border: 2px solid var(--primary-700); 19 | } 20 | } 21 | 22 | .search-bar-input-label { 23 | display: block; 24 | font-size: var(--font-md); 25 | font-weight: var(--weight-semibold); 26 | color: var(--font-color); 27 | margin: pxToRem(12) 0 pxToRem(4) pxToRem(4); 28 | } 29 | -------------------------------------------------------------------------------- /app/components/ui/select/index.jsx: -------------------------------------------------------------------------------- 1 | // import scss 2 | import './select.scss'; 3 | 4 | // import components 5 | import Container from './container'; 6 | import Item from './item'; 7 | import List from './list'; 8 | import Trigger from './trigger'; 9 | 10 | const Select = { 11 | Container, 12 | Item, 13 | List, 14 | Trigger, 15 | }; 16 | 17 | export { Container, Item, List, Trigger }; 18 | 19 | export default Select; 20 | -------------------------------------------------------------------------------- /app/components/ui/select/item.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import classNames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // import local files 6 | import { colorPropType } from '../../../../shared/utils/propTypes'; 7 | 8 | const Item = ({ 9 | dotColor, 10 | showDot = false, 11 | isSelected = false, 12 | children, 13 | as: Wrapper = 'div', 14 | ...props 15 | }) => { 16 | const classes = classNames('select-item-dot', { 17 | 'select-item-selected': isSelected, 18 | [`select-item-dot--${dotColor}`]: dotColor, 19 | }); 20 | 21 | return ( 22 | 23 | {showDot && } 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | Item.displayName = 'Select.Item'; 30 | 31 | Item.propTypes = { 32 | dotColor: colorPropType, 33 | showDot: PropTypes.bool, 34 | isSelected: PropTypes.bool, 35 | children: PropTypes.node, 36 | as: PropTypes.elementType, 37 | }; 38 | 39 | export default Item; 40 | -------------------------------------------------------------------------------- /app/components/ui/select/list.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import TextInput from '../input'; 5 | 6 | const List = ({ 7 | fullWidth, 8 | isOpen, 9 | selectSearch, 10 | handleSearch, 11 | children, 12 | ...props 13 | }) => { 14 | const classes = classNames('select-body', { 15 | 'select-list-full-width': fullWidth, 16 | 'select-body-open': isOpen, 17 | }); 18 | 19 | return ( 20 |
21 |
22 | 23 |
24 | {children} 25 |
26 | ); 27 | }; 28 | 29 | List.displayName = 'Select.List'; 30 | 31 | List.propTypes = { 32 | fullWidth: PropTypes.bool, 33 | isOpen: PropTypes.bool, 34 | selectSearch: PropTypes.string, 35 | handleSearch: PropTypes.func, 36 | children: PropTypes.node, 37 | }; 38 | 39 | export default List; 40 | -------------------------------------------------------------------------------- /app/components/ui/select/trigger.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import classNames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // import local files 6 | import { FaChevronUp } from '../../icons'; 7 | 8 | const Trigger = ({ 9 | asInput, 10 | isOpen, 11 | icon, 12 | showIcon, 13 | toggleSelect, 14 | children, 15 | ...props 16 | }) => { 17 | const classes = classNames('select-trigger', { 18 | 'select-trigger-input': asInput, 19 | }); 20 | 21 | const iconClasses = classNames('select-trigger-icon', { 22 | open: isOpen, 23 | }); 24 | 25 | return ( 26 |
27 | {children} 28 | {showIcon && ( 29 |
30 | {icon || } 31 |
32 | )} 33 |
34 | ); 35 | }; 36 | 37 | Trigger.displayName = 'Select.Trigger'; 38 | 39 | Trigger.propTypes = { 40 | asInput: PropTypes.bool, 41 | isOpen: PropTypes.bool.isRequired, 42 | icon: PropTypes.node, 43 | showIcon: PropTypes.bool, 44 | toggleSelect: PropTypes.func.isRequired, 45 | children: PropTypes.node.isRequired, 46 | }; 47 | 48 | export default Trigger; 49 | -------------------------------------------------------------------------------- /app/components/ui/spacer.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | const Spacer = ({ size = 0 }) => ( 5 |
6 | Hidden text 7 |
8 | ); 9 | 10 | Spacer.propTypes = { 11 | size: PropTypes.number, 12 | }; 13 | 14 | export default Spacer; 15 | -------------------------------------------------------------------------------- /app/components/ui/statusBar.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .status-bar-container { 4 | display: grid; 5 | direction: rtl; 6 | gap: pxToRem(8); 7 | } 8 | 9 | .status-bar { 10 | background-color: red; 11 | width: 100%; 12 | border-radius: pxToRem(12); 13 | height: pxToRem(25); 14 | background-color: var(--accent-600); 15 | 16 | &:hover { 17 | cursor: pointer; 18 | margin: 0; 19 | border-radius: pxToRem(4); 20 | } 21 | 22 | &.status-bar-healthy { 23 | background-color: var(--green-600); 24 | } 25 | 26 | &.status-bar-alert { 27 | background-color: var(--red-600); 28 | } 29 | 30 | &.status-bar-unknown { 31 | background-color: var(--gray-600); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/components/ui/table/body.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Body = ({ children }) => { 4 | return {children}; 5 | }; 6 | 7 | Body.propTypes = { 8 | children: PropTypes.node, 9 | }; 10 | 11 | Body.displayName = 'TableBody'; 12 | 13 | export default Body; 14 | -------------------------------------------------------------------------------- /app/components/ui/table/caption.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Caption = ({ children }) => { 4 | return {children}; 5 | }; 6 | 7 | Caption.propTypes = { 8 | children: PropTypes.node, 9 | }; 10 | 11 | Caption.displayName = 'TableCaption'; 12 | 13 | export default Caption; 14 | -------------------------------------------------------------------------------- /app/components/ui/table/cell.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Cell = ({ children, align = 'left', ...props }) => { 5 | const classes = classNames('table', { 6 | [`align-${align}`]: true, 7 | }); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | Cell.propTypes = { 17 | children: PropTypes.node, 18 | align: PropTypes.oneOf(['left', 'center', 'right']), 19 | }; 20 | 21 | Cell.displayName = 'TableCell'; 22 | 23 | export default Cell; 24 | -------------------------------------------------------------------------------- /app/components/ui/table/footer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Footer = ({ children }) => { 4 | return {children}; 5 | }; 6 | 7 | Footer.propTypes = { 8 | children: PropTypes.node, 9 | }; 10 | 11 | Footer.displayName = 'TableFooter'; 12 | 13 | export default Footer; 14 | -------------------------------------------------------------------------------- /app/components/ui/table/head.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Head = ({ children, align = 'left', ...props }) => { 5 | const classes = classNames('table', { 6 | [`align-${align}`]: true, 7 | }); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | Head.propTypes = { 17 | children: PropTypes.node, 18 | align: PropTypes.oneOf(['left', 'center', 'right']), 19 | }; 20 | 21 | Head.displayName = 'TableHead'; 22 | 23 | export default Head; 24 | -------------------------------------------------------------------------------- /app/components/ui/table/header.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Header = ({ children }) => { 4 | return {children}; 5 | }; 6 | 7 | Header.propTypes = { 8 | children: PropTypes.node, 9 | }; 10 | 11 | Header.displayName = 'TableHeader'; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /app/components/ui/table/index.jsx: -------------------------------------------------------------------------------- 1 | import './styles.scss'; 2 | 3 | // import local files 4 | import Body from './body'; 5 | import Caption from './caption'; 6 | import Cell from './cell'; 7 | import Footer from './footer'; 8 | import Head from './head'; 9 | import Header from './header'; 10 | import Row from './row'; 11 | import TableContainer from './table'; 12 | 13 | const Table = { 14 | Body, 15 | Caption, 16 | Cell, 17 | Footer, 18 | Head, 19 | Header, 20 | Row, 21 | Table: TableContainer, 22 | }; 23 | 24 | export { 25 | Body, 26 | Caption, 27 | Cell, 28 | Footer, 29 | Head, 30 | Header, 31 | Row, 32 | TableContainer as Table, 33 | }; 34 | 35 | Table.displayName = 'Table'; 36 | 37 | export default Table; 38 | -------------------------------------------------------------------------------- /app/components/ui/table/row.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Row = ({ children, ...props }) => { 4 | return {children}; 5 | }; 6 | 7 | Row.propTypes = { 8 | children: PropTypes.node, 9 | }; 10 | 11 | Row.displayName = 'TableRow'; 12 | 13 | export default Row; 14 | -------------------------------------------------------------------------------- /app/components/ui/table/styles.scss: -------------------------------------------------------------------------------- 1 | .table-container { 2 | overflow: auto; 3 | border-radius: 12px 12px 0 0; 4 | border-bottom: 2px solid var(--accent-900); 5 | } 6 | 7 | table { 8 | border-collapse: collapse; 9 | border-style: hidden; 10 | border-spacing: 0; 11 | 12 | width: 100%; 13 | max-width: 100%; 14 | } 15 | 16 | caption { 17 | caption-side: bottom; 18 | padding: 10px; 19 | font-weight: bold; 20 | } 21 | 22 | thead, 23 | tfoot { 24 | background-color: var(--accent-900); 25 | } 26 | 27 | thead { 28 | overflow: hidden; 29 | } 30 | 31 | th { 32 | padding: 16px 10px; 33 | } 34 | td { 35 | padding: 8px 10px; 36 | } 37 | 38 | td { 39 | border-bottom: 2px solid var(--accent-900); 40 | } 41 | 42 | tbody tr { 43 | transition: background-color 0.2s ease-in-out; 44 | 45 | &:hover { 46 | cursor: pointer; 47 | background-color: var(--accent-700); 48 | } 49 | } 50 | 51 | .table { 52 | &.align-left { 53 | text-align: left; 54 | } 55 | 56 | &.align-center { 57 | text-align: center; 58 | } 59 | 60 | &.align-right { 61 | text-align: right; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/components/ui/table/table.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const Table = ({ children }) => { 4 | return ( 5 |
6 | {children}
7 |
8 | ); 9 | }; 10 | 11 | Table.propTypes = { 12 | children: PropTypes.node, 13 | }; 14 | 15 | Table.displayName = 'TableTable'; 16 | 17 | export default Table; 18 | -------------------------------------------------------------------------------- /app/components/ui/textarea.jsx: -------------------------------------------------------------------------------- 1 | import './textarea.scss'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Textarea = ({ label, error, id = 'text-input', children, ...props }) => { 5 | return ( 6 |
7 | {label && } 8 | 11 | {error && ( 12 | 13 | {error} 14 | 15 | )} 16 |
17 | ); 18 | }; 19 | 20 | Textarea.displayName = 'Textarea'; 21 | 22 | Textarea.propTypes = { 23 | label: PropTypes.string, 24 | error: PropTypes.string, 25 | id: PropTypes.string, 26 | children: PropTypes.node, 27 | }; 28 | 29 | export default Textarea; 30 | -------------------------------------------------------------------------------- /app/components/ui/textarea.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/pxToRem.scss' as *; 2 | 3 | .textarea { 4 | width: 100%; 5 | background-color: var(--accent-800); 6 | border: none; 7 | outline: none; 8 | color: var(--font-color); 9 | font-size: var(--font-md); 10 | font-weight: var(--weight-medium); 11 | border-radius: var(--radius-md); 12 | border: 2px solid #ffffff00; 13 | padding: pxToRem(8); 14 | font-family: var(--font-family); 15 | 16 | &:focus { 17 | border: 2px solid var(--primary-700); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/tooltip.jsx: -------------------------------------------------------------------------------- 1 | import './tooltip.scss'; 2 | 3 | // import dependencies 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | const Tooltip = ({ children, text, position = 'top', color }) => { 8 | const classes = classNames('tooltip', `tooltip--${position}`, { 9 | [`tooltip--${color}`]: color, 10 | }); 11 | 12 | return ( 13 |
14 |
{text}
15 | {children} 16 |
17 | ); 18 | }; 19 | 20 | Tooltip.displayName = 'Tooltip'; 21 | 22 | Tooltip.propTypes = { 23 | text: PropTypes.string, 24 | position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), 25 | children: PropTypes.node, 26 | }; 27 | 28 | export default Tooltip; 29 | -------------------------------------------------------------------------------- /app/constant/dateformats.json: -------------------------------------------------------------------------------- 1 | [ 2 | "DD/MM/YYYY", 3 | "MM/DD/YYYY", 4 | "YYYY/MM/DD", 5 | "DD-MM-YYYY", 6 | "MM-DD-YYYY", 7 | "YYYY-MM-DD", 8 | "DD/MM/YY", 9 | "MM/DD/YY", 10 | "YY/MM/DD", 11 | "DD-MM-YY", 12 | "MM-DD-YY", 13 | "YY-MM-DD" 14 | ] 15 | -------------------------------------------------------------------------------- /app/constant/notifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "Discord": { "name": "Discord", "icon": "discord.svg" }, 3 | "Slack": { "name": "Slack", "icon": "slack.svg" }, 4 | "Telegram": { "name": "Telegram", "icon": "telegram.svg" }, 5 | "Webhook": { "name": "Webhook", "icon": "webhook.svg" } 6 | } 7 | -------------------------------------------------------------------------------- /app/context/index.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | // Global stores 4 | import GlobalStore from './global'; 5 | import ModalStore from './modal'; 6 | import UserStore from './user'; 7 | import NotificationStore from './notifications'; 8 | import StatusStore from './status'; 9 | 10 | const store = { 11 | globalStore: new GlobalStore(), 12 | notificationStore: new NotificationStore(), 13 | modalStore: new ModalStore(), 14 | userStore: new UserStore(), 15 | statusStore: new StatusStore(), 16 | }; 17 | 18 | const ContextStore = createContext(store); 19 | const useContextStore = () => useContext(ContextStore); 20 | 21 | export default useContextStore; 22 | -------------------------------------------------------------------------------- /app/context/modal.js: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from 'mobx'; 2 | 3 | class ModalStore { 4 | constructor() { 5 | this.isOpen = false; 6 | this.content = null; 7 | this.glassmorph = true; 8 | makeObservable(this, { 9 | isOpen: observable, 10 | openModal: action, 11 | closeModal: action, 12 | }); 13 | } 14 | 15 | openModal = (content, glassmorph = true) => { 16 | this.isOpen = true; 17 | this.content = content; 18 | this.glassmorph = glassmorph; 19 | }; 20 | 21 | closeModal = () => { 22 | this.isOpen = false; 23 | this.content = null; 24 | this.glassmorph = true; 25 | }; 26 | } 27 | 28 | export default ModalStore; 29 | -------------------------------------------------------------------------------- /app/context/status.js: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | 3 | class StatusStore { 4 | constructor() { 5 | this.statusPages = new Map(); 6 | 7 | makeObservable(this, { 8 | statusPages: observable, 9 | setStatusPages: action, 10 | addStatusPage: action, 11 | deleteStatusPage: action, 12 | getStatusById: action, 13 | getStatusByUrl: action, 14 | allStatusPages: computed, 15 | }); 16 | } 17 | 18 | setStatusPages = (statusPages) => { 19 | for (const statusPage of statusPages) { 20 | this.statusPages.set(statusPage.statusId, statusPage); 21 | } 22 | }; 23 | 24 | addStatusPage = (statusPage) => { 25 | this.statusPages.set(statusPage.statusId, statusPage); 26 | }; 27 | 28 | deleteStatusPage = (id) => { 29 | this.statusPages.delete(id); 30 | }; 31 | 32 | getStatusById = (id) => { 33 | return this.statusPages.get(id); 34 | }; 35 | 36 | getStatusByUrl = (url) => { 37 | return Array.from(this.statusPages.values()).find( 38 | (statusPage) => statusPage.statusUrl === url 39 | ); 40 | }; 41 | 42 | get allStatusPages() { 43 | return Array.from(this.statusPages.values()) || []; 44 | } 45 | } 46 | 47 | export default StatusStore; 48 | -------------------------------------------------------------------------------- /app/context/team.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { action, computed, makeObservable, observable } from 'mobx'; 3 | 4 | class TeamStore { 5 | constructor() { 6 | this.team = new Map(); 7 | makeObservable(this, { 8 | team: observable, 9 | setTeam: action, 10 | updateUserPermission: action, 11 | updateUserVerified: action, 12 | removeUser: action, 13 | teamMembers: computed, 14 | }); 15 | } 16 | 17 | get teamMembers() { 18 | return Array.from(this.team.values()); 19 | } 20 | 21 | setTeam = (data) => { 22 | for (const user of data) { 23 | this.team.set(user.email, user); 24 | } 25 | }; 26 | 27 | updateUserPermission = (email, permission) => { 28 | const user = this.team.get(email); 29 | 30 | user.permission = permission; 31 | this.team.set(email, user); 32 | }; 33 | 34 | updateUserVerified = (email) => { 35 | const user = this.team.get(email); 36 | 37 | user.isVerified = true; 38 | this.team.set(email, user); 39 | }; 40 | 41 | removeUser = (email) => { 42 | this.team.delete(email); 43 | }; 44 | } 45 | 46 | const team = new TeamStore(); 47 | const store = createContext(team); 48 | const useTeamContext = () => useContext(store); 49 | 50 | export default useTeamContext; 51 | -------------------------------------------------------------------------------- /app/context/user.js: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from 'mobx'; 2 | 3 | export default class UserStore { 4 | constructor() { 5 | this.user = {}; 6 | makeObservable(this, { 7 | user: observable, 8 | setUser: action, 9 | updateUsingKey: action, 10 | updateUser: action, 11 | }); 12 | } 13 | 14 | setUser = (user) => { 15 | this.user = user; 16 | }; 17 | 18 | updateUser = (data) => { 19 | this.user = data; 20 | }; 21 | 22 | updateUsingKey = (key, value) => { 23 | this.user[key] = value; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/handlers/login.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { toast } from 'react-toastify'; 3 | 4 | // import local files 5 | import { createPostRequest } from '../services/axios'; 6 | import validators from '../../shared/validators'; 7 | 8 | const handleLogin = async (inputs, setErrors, navigate) => { 9 | try { 10 | const { email, password } = inputs; 11 | 12 | const hasInvalidData = 13 | validators.auth.email(email) || validators.auth.password(password); 14 | 15 | if (hasInvalidData) { 16 | return setErrors(hasInvalidData); 17 | } 18 | 19 | const query = await createPostRequest('/auth/login', { email, password }); 20 | 21 | if (query.status === 200) { 22 | return navigate('/home'); 23 | } 24 | } catch (error) { 25 | if (error.response?.status === 418) { 26 | return navigate('/verify'); 27 | } 28 | 29 | if (error.response?.data?.message) { 30 | return setErrors({ general: error.response?.data?.message }); 31 | } 32 | 33 | toast.error('Something went wrong'); 34 | } 35 | }; 36 | 37 | export default handleLogin; 38 | -------------------------------------------------------------------------------- /app/handlers/register.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { toast } from 'react-toastify'; 3 | 4 | // import local files 5 | import { createPostRequest } from '../services/axios'; 6 | import validators from '../../shared/validators'; 7 | 8 | const handleRegister = async (inputs, setErrors, setPage, navigate) => { 9 | try { 10 | const { email, username, password, confirmPassword } = inputs; 11 | 12 | const isInvalidPassword = validators.auth.password(password); 13 | 14 | if (isInvalidPassword) { 15 | return setErrors(isInvalidPassword); 16 | } 17 | 18 | if (password !== confirmPassword) { 19 | return setErrors({ confirmPassword: 'Passwords do not match' }); 20 | } 21 | 22 | const query = await createPostRequest('/auth/register', { 23 | email, 24 | username, 25 | password, 26 | }); 27 | 28 | toast.success('You have been successfully registered!'); 29 | 30 | if (query.status === 201) { 31 | return navigate('/home'); 32 | } 33 | 34 | setPage('verify'); 35 | } catch (error) { 36 | if (error?.response?.data?.message) { 37 | return setErrors(error?.response?.data?.message); 38 | } 39 | 40 | toast.error('Something went wrong!'); 41 | } 42 | }; 43 | 44 | export default handleRegister; 45 | -------------------------------------------------------------------------------- /app/handlers/settings/account/transfer.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { toast } from 'react-toastify'; 3 | 4 | // import local files 5 | import { createPostRequest } from '../../../services/axios'; 6 | 7 | const handleTransferAccount = async (email, closeModal) => { 8 | try { 9 | const query = await createPostRequest('/api/user/transfer/ownership', { 10 | email, 11 | }); 12 | 13 | if (query.status === 200) { 14 | toast.success('Ownership successfully transferred!'); 15 | closeModal(); 16 | } 17 | } catch { 18 | toast.error('Something went wrong, please try again later.'); 19 | } 20 | }; 21 | 22 | export default handleTransferAccount; 23 | -------------------------------------------------------------------------------- /app/handlers/settings/account/username.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { toast } from 'react-toastify'; 3 | 4 | // import local files 5 | import validators from '../../../../shared/validators'; 6 | import { createPostRequest } from '../../../services/axios'; 7 | 8 | const handleChangeUsername = async (displayName, handleError, closeModal) => { 9 | try { 10 | const isInvalid = validators.auth.username(displayName); 11 | 12 | if (isInvalid) { 13 | return handleError(isInvalid.username); 14 | } 15 | 16 | const query = await createPostRequest('/api/user/update/username', { 17 | displayName, 18 | }); 19 | 20 | if (query.status === 200) { 21 | toast.success('Username changed successfully!'); 22 | closeModal(); 23 | } 24 | 25 | return true; 26 | } catch (error) { 27 | if (error.response?.data?.username) { 28 | return handleError(error.response?.data?.username); 29 | } 30 | 31 | toast.error('Something went wrong, please try again later.'); 32 | 33 | return false; 34 | } 35 | }; 36 | 37 | export default handleChangeUsername; 38 | -------------------------------------------------------------------------------- /app/handlers/setup.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { toast } from 'react-toastify'; 3 | 4 | // import local files 5 | import { createPostRequest } from '../services/axios'; 6 | import { getSetupKeys } from '../../shared/data/setup'; 7 | import setupValidators from '../../shared/validators/setup'; 8 | 9 | const submitSetup = async (setErrors, type = 'basic', inputs) => { 10 | const keys = getSetupKeys(type); 11 | 12 | let errors = false; 13 | for (let key of keys) { 14 | const validator = setupValidators[key]; 15 | errors = validator(inputs[key], setErrors); 16 | 17 | if (errors) break; 18 | } 19 | 20 | if (errors) throw new Error('Issue occured while setting up configuration'); 21 | 22 | await createPostRequest('/auth/setup', { ...inputs, type }); 23 | toast.success('Setup configuration successful'); 24 | }; 25 | 26 | export default submitSetup; 27 | -------------------------------------------------------------------------------- /app/handlers/status/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/app/handlers/status/index.js -------------------------------------------------------------------------------- /app/hooks/useDropdown.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { useState } from 'react'; 3 | 4 | const useDropdown = (closeOnSelect, defaultValue = null) => { 5 | const [values, setValues] = useState({ 6 | isOpen: false, 7 | selectedId: defaultValue, 8 | }); 9 | 10 | const toggleDropdown = () => { 11 | return setValues((prev) => ({ ...prev, isOpen: !prev.isOpen })); 12 | }; 13 | 14 | const handleDropdownSelect = (id) => { 15 | if (closeOnSelect) { 16 | return setValues((prev) => ({ 17 | selectedId: id, 18 | isOpen: !prev.isOpen, 19 | })); 20 | } 21 | 22 | return setValues((prev) => ({ ...prev, selectedId: id })); 23 | }; 24 | 25 | return { 26 | dropdownIsOpen: values.isOpen, 27 | selectedId: values.selectedId, 28 | handleDropdownSelect, 29 | toggleDropdown, 30 | }; 31 | }; 32 | 33 | export default useDropdown; 34 | -------------------------------------------------------------------------------- /app/hooks/useGoBack.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from 'react-router-dom'; 2 | 3 | /** 4 | * A function that returns a function to navigate back to the previous location or a fallback location. 5 | * 6 | * @param {Object} prev - an object containing the previous location and fallback location 7 | * @return {void} 8 | */ 9 | const useGoBack = () => { 10 | const { key: prevKey } = useLocation(); 11 | const navigate = useNavigate(); 12 | 13 | return ({ fallback = '/home' } = {}) => { 14 | const key = prevKey !== 'default' ? -1 : fallback; 15 | navigate(key); 16 | }; 17 | }; 18 | 19 | export default useGoBack; 20 | -------------------------------------------------------------------------------- /app/hooks/useSelect.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import { useState } from 'react'; 3 | 4 | const useSelect = (closeOnSelect, defaultValue = []) => { 5 | const [values, setValues] = useState({ 6 | isOpen: false, 7 | selectedIds: defaultValue, 8 | search: '', 9 | }); 10 | 11 | const toggleSelect = () => { 12 | return setValues((prev) => ({ ...prev, isOpen: !prev.isOpen })); 13 | }; 14 | 15 | const handleSearch = (e) => { 16 | return setValues((prev) => ({ ...prev, search: e.target.value })); 17 | }; 18 | 19 | const handleItemSelect = (id) => { 20 | if (closeOnSelect) { 21 | return setValues((prev) => ({ 22 | ...prev, 23 | selectedIds: prev.selectedIds.push(id), 24 | isOpen: !prev.isOpen, 25 | })); 26 | } 27 | 28 | return setValues((prev) => ({ 29 | ...prev, 30 | selectedIds: prev.selectedIds.push(id), 31 | })); 32 | }; 33 | 34 | return { 35 | selectIsOpen: values.isOpen, 36 | selectedIds: values.selectedIds, 37 | selectSearch: values.search, 38 | handleSearch, 39 | handleItemSelect, 40 | toggleSelect, 41 | }; 42 | }; 43 | 44 | export default useSelect; 45 | -------------------------------------------------------------------------------- /app/layout/status.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import PropTypes from 'prop-types'; 3 | 4 | // import local files 5 | import { 6 | LocalStorageStateProvider, 7 | useLocalStorageState, 8 | } from '../hooks/useLocalstorage'; 9 | 10 | const StatusLayout = ({ children }) => { 11 | const localStorageState = useLocalStorageState(); 12 | 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | StatusLayout.displayName = 'StatusLayout'; 21 | 22 | StatusLayout.propTypes = { 23 | children: PropTypes.node, 24 | }; 25 | 26 | export default StatusLayout; 27 | -------------------------------------------------------------------------------- /app/pages/error.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/pages/error.scss'; 2 | 3 | // import dependencies 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | // import local files 7 | import Button from '../components/ui/button'; 8 | 9 | const ErrorPage = () => { 10 | const navigate = useNavigate(); 11 | return ( 12 |
13 |
14 |
4
15 | 16 |
4
17 |
18 |
19 | Sorry, couldn't find what you're looking for! 20 |
21 | 24 |
25 | ); 26 | }; 27 | 28 | export default ErrorPage; 29 | -------------------------------------------------------------------------------- /app/pages/setup.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/pages/register.scss'; 2 | 3 | // import local files 4 | import { useSetup } from '../hooks/useSetup'; 5 | import { SetupFormStateProvider } from '../hooks/useSetup'; 6 | import SetupForm from '../components/setup'; 7 | 8 | const Setup = () => { 9 | const setupValues = useSetup(); 10 | 11 | return ( 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Setup; 23 | -------------------------------------------------------------------------------- /app/pages/verify.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/pages/verify.scss'; 2 | 3 | import { StatusLogo } from '../components/icons'; 4 | 5 | const Verify = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | Please contact the owner/admins of this dashboard to verify your 13 | account. 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Verify; 20 | -------------------------------------------------------------------------------- /app/services/monitor/fetch.js: -------------------------------------------------------------------------------- 1 | import { createGetRequest } from '../axios'; 2 | 3 | const fetchMonitorById = async (monitorId, func) => { 4 | const monitor = await createGetRequest('/api/monitor/id', { monitorId }); 5 | 6 | if (func) { 7 | func(monitor.data, fetchMonitorById); 8 | } 9 | 10 | return monitor.data; 11 | }; 12 | 13 | export { fetchMonitorById }; 14 | -------------------------------------------------------------------------------- /app/styles/breakpoints.scss: -------------------------------------------------------------------------------- 1 | @use './pxToRem.scss' as *; 2 | 3 | $BREAKPOINT_1: pxToRem(480); 4 | $BREAKPOINT_2: pxToRem(768); 5 | $BREAKPOINT_3: pxToRem(1024); 6 | $BREAKPOINT_4: pxToRem(1200); 7 | $BREAKPOINT_5: pxToRem(1440); 8 | $BREAKPOINT_6: pxToRem(1920); 9 | $HALF_WIDTH: 50%; 10 | $FULL_WIDTH: 100%; 11 | 12 | @mixin desktop() { 13 | @media (max-width: $BREAKPOINT_6) { 14 | @content; 15 | } 16 | } 17 | 18 | @mixin laptop() { 19 | @media (max-width: $BREAKPOINT_4) { 20 | @content; 21 | } 22 | } 23 | 24 | @mixin tablet() { 25 | @media (max-width: $BREAKPOINT_3) { 26 | @content; 27 | } 28 | } 29 | 30 | @mixin mobile() { 31 | @media (max-width: $BREAKPOINT_2) { 32 | @content; 33 | } 34 | } 35 | 36 | @mixin Container() { 37 | display: flex; 38 | margin: auto; 39 | max-width: $BREAKPOINT_6; 40 | 41 | @media (max-width: $BREAKPOINT_6) { 42 | max-width: 1500px; 43 | } 44 | 45 | @media (max-width: $BREAKPOINT_4) { 46 | max-width: $BREAKPOINT_3; 47 | } 48 | 49 | @media (max-width: $BREAKPOINT_3) { 50 | max-width: 968px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/styles/font.scss: -------------------------------------------------------------------------------- 1 | @use './pxToRem.scss' as *; 2 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 3 | 4 | :root { 5 | --font-xs: #{pxToRem(12)}; 6 | --font-sm: #{pxToRem(14)}; 7 | --font-md: #{pxToRem(16)}; 8 | --font-lg: #{pxToRem(18)}; 9 | --font-xl: #{pxToRem(20)}; 10 | --font-2xl: #{pxToRem(24)}; 11 | --font-3xl: #{pxToRem(30)}; 12 | --font-4xl: #{pxToRem(36)}; 13 | --font-5xl: #{pxToRem(48)}; 14 | --font-6xl: #{pxToRem(60)}; 15 | --font-7xl: #{pxToRem(72)}; 16 | --font-8xl: #{pxToRem(96)}; 17 | --font-9xl: #{pxToRem(128)}; 18 | 19 | --weight-hairline: 100; 20 | --weight-thin: 200; 21 | --weight-light: 300; 22 | --weight-normal: 400; 23 | --weight-medium: 500; 24 | --weight-semibold: 600; 25 | --weight-bold: 700; 26 | --weight-extrabold: 800; 27 | --weight-black: 900; 28 | 29 | --font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', 30 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 31 | 'Helvetica Neue', sans-serif; 32 | } 33 | -------------------------------------------------------------------------------- /app/styles/pages/error.scss: -------------------------------------------------------------------------------- 1 | @use '../breakpoints.scss' as *; 2 | 3 | .error-page-container { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | height: 100vh; 9 | } 10 | 11 | .error-page-header { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | font-size: 12rem; 16 | gap: 8px; 17 | font-weight: 600; 18 | color: var(--accent-500); 19 | } 20 | 21 | .error-page-header-image { 22 | width: 12rem; 23 | height: 12rem; 24 | } 25 | 26 | .error-page-subtitle { 27 | font-size: 1.5rem; 28 | font-weight: 600; 29 | margin-bottom: 20px; 30 | text-align: center; 31 | } 32 | 33 | @include mobile { 34 | .error-page-header { 35 | font-size: 8rem; 36 | } 37 | 38 | .error-page-header-image { 39 | width: 8rem; 40 | height: 8rem; 41 | } 42 | 43 | .error-page-subtitle { 44 | font-size: 1.2rem; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/styles/pages/home.scss: -------------------------------------------------------------------------------- 1 | @use '../pxToRem.scss' as *; 2 | 3 | .home-container { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-content: flex-start; 7 | padding: pxToRem(16) pxToRem(12); 8 | gap: pxToRem(16); 9 | overflow-y: auto; 10 | padding-bottom: pxToRem(48); 11 | } 12 | -------------------------------------------------------------------------------- /app/styles/pages/monitor.scss: -------------------------------------------------------------------------------- 1 | @use '../pxToRem.scss' as *; 2 | 3 | .monitor-container { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | gap: pxToRem(16); 8 | margin: pxToRem(16); 9 | } 10 | -------------------------------------------------------------------------------- /app/styles/pages/notifications.scss: -------------------------------------------------------------------------------- 1 | .notification-empty { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | width: 100%; 8 | gap: 10px; 9 | flex: 1; 10 | 11 | &-icon { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | color: var(--accent-200); 16 | } 17 | 18 | &-text { 19 | font-size: 1.5rem; 20 | color: var(--primary-500); 21 | } 22 | } 23 | 24 | .notification-container { 25 | display: flex; 26 | flex-direction: row; 27 | flex-wrap: wrap; 28 | gap: 10px; 29 | padding: 10px; 30 | margin: 10px 0; 31 | } 32 | -------------------------------------------------------------------------------- /app/styles/pages/verify.scss: -------------------------------------------------------------------------------- 1 | .verify-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | width: 100vw; 7 | height: 100vh; 8 | } 9 | 10 | .verify-description { 11 | text-align: center; 12 | font-size: 24px; 13 | width: 450px; 14 | } 15 | -------------------------------------------------------------------------------- /app/styles/pxToRem.scss: -------------------------------------------------------------------------------- 1 | @function pxToRem($sizeInPixels) { 2 | $result: calc($sizeInPixels / 16) + rem; 3 | @return $result; 4 | } 5 | -------------------------------------------------------------------------------- /app/styles/radius.scss: -------------------------------------------------------------------------------- 1 | @use './pxToRem.scss' as *; 2 | 3 | :root { 4 | --radius-xs: #{pxToRem(5)}; 5 | --radius-sm: #{pxToRem(8)}; 6 | --radius-md: #{pxToRem(12)}; 7 | --radius-lg: #{pxToRem(18)}; 8 | --radius-xl: #{pxToRem(24)}; 9 | --radius-2xl: #{pxToRem(32)}; 10 | --radius-squared: 33%; 11 | --radius-rounded: 50%; 12 | --radius-pill: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /app/styles/shadows.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --shadow-sm: 0px 0px 5px 0px rgb(0 0 0 / 0.05), 3 | 0px 2px 10px 0px rgb(0 0 0 / 0.2), 4 | inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); 5 | 6 | --shadow-md: 0px 0px 15px 0px rgb(0 0 0 / 0.06), 7 | 0px 2px 30px 0px rgb(0 0 0 / 0.22), 8 | inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); 9 | 10 | --shadow-lg: 0px 0px 30px 0px rgb(0 0 0 / 0.07), 11 | 0px 30px 60px 0px rgb(0 0 0 / 0.26), 12 | inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); 13 | } 14 | -------------------------------------------------------------------------------- /app/styles/transitions.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --transition-slowest: all 1s ease-in-out; 3 | --transition-slow: all 0.5s ease-in-out; 4 | --transition-base: all 0.3s ease-in-out; 5 | --transition-fast: all 0.1s ease-in-out; 6 | --transition-fastest: all 0.05s ease-in-out; 7 | } 8 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:2308', 6 | viewportWidth: 1920, 7 | viewportHeight: 1080, 8 | specPattern: 'test/e2e/**/*.test.{js,jsx,ts,tsx}', 9 | fixturesFolder: 'test/e2e/setup/fixtures', 10 | screenshotsFolder: 'test/e2e/setup/screenshots', 11 | videosFolder: 'test/e2e/setup/videos', 12 | downloadsFolder: 'test/e2e/setup/downloads', 13 | supportFile: 'test/e2e/setup/support/e2e.js', 14 | experimentalRunAllSpecs: true, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme'; 2 | import './style.css'; 3 | 4 | export default Theme; 5 | -------------------------------------------------------------------------------- /docs/components/DividePage.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /docs/components/kanban/badge.vue: -------------------------------------------------------------------------------- 1 | 9 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /docs/contributing/conduct.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/contributing/conduct.md -------------------------------------------------------------------------------- /docs/contributing/issues.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/contributing/issues.md -------------------------------------------------------------------------------- /docs/contributing/overview.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/contributing/overview.md -------------------------------------------------------------------------------- /docs/contributing/testing.md: -------------------------------------------------------------------------------- 1 | # Testing requirements 2 | 3 | ## End to end testing 4 | 5 | For the meantime, end to end testing will be limited to monitor adding/removing, registering a user, signing in and verifying a user. We may expand this in future. If you have made any changes to these functionalities, please make sure end to end are passing and if need be, please update them according to your changes. 6 | 7 | #### Test files: 8 | 9 | - Monitor add/remove 10 | - Register 11 | - Signin 12 | - Verify 13 | -------------------------------------------------------------------------------- /docs/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | layout: page 5 | prev: false 6 | next: false 7 | --- 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/guides/slack/create-webhook.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | prev: 5 | text: 'Guides' 6 | link: '/guides' 7 | next: false 8 | --- 9 | 10 | ## Currently a work in progress. 11 | 12 | For now check out the [Slack official documentation](https://api.slack.com/messaging/webhooks) for more information. 13 | -------------------------------------------------------------------------------- /docs/guides/telegram/create-bot.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | prev: 5 | text: 'Guides' 6 | link: '/guides' 7 | next: false 8 | --- 9 | 10 | ## Currently a work in progress. 11 | 12 | For now check out the [Telegram official documentation](https://core.telegram.org/bots/tutorial) for more information. 13 | -------------------------------------------------------------------------------- /docs/guides/telegram/find-chat-id.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | prev: 5 | text: 'Guides' 6 | link: '/guides' 7 | next: false 8 | --- 9 | -------------------------------------------------------------------------------- /docs/guides/webhook/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | prev: 5 | text: 'Guides' 6 | link: '/guides' 7 | next: false 8 | --- 9 | 10 | ## Currently a work in progress. 11 | 12 | You can create webhooks using various different services. Below are a few services that you can possibly use. 13 | 14 | - [Zapier](https://ifttt.com/) 15 | - [IFTTT](https://zapier.com/) 16 | -------------------------------------------------------------------------------- /docs/internals/components.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/internals/components.md -------------------------------------------------------------------------------- /docs/internals/flows.md: -------------------------------------------------------------------------------- 1 | # Flows 2 | 3 | ## Register 4 | 5 | ## Signin 6 | 7 | ## Add/Edit Monitor 8 | 9 | ## Reset Password 10 | -------------------------------------------------------------------------------- /docs/internals/notifications.md: -------------------------------------------------------------------------------- 1 | # Notifications 2 | 3 | Notifcations are currently a work in progress and will be available soon. Once we have added the notifications feature, this page will be updated. 4 | 5 | Currently we are planning on introducing notifications for Discord, Slack, Teams, Telegram, and Webhooks. If you would like to add other notification systems, please feel free to [open an issue](https://github.com/Lunalytics/lunalytics/issues) or [create a pull request](https://github.com/Lunalytics/lunalytics/pulls). 6 | 7 | ## Discord 8 | 9 | Currently a work in progress 10 | 11 | ## Slack 12 | 13 | Currently a work in progress 14 | 15 | ## Teams 16 | 17 | Currently a work in progress 18 | 19 | ## Telegram 20 | 21 | Currently a work in progress 22 | 23 | ## Webhooks 24 | 25 | Currently a work in progress 26 | -------------------------------------------------------------------------------- /docs/internals/overview.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/internals/overview.md -------------------------------------------------------------------------------- /docs/internals/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | --- 4 | 5 | 8 | 9 | # Roadmap 10 | 11 | The following roadmap contains all the updates I want to push out before the full release. Currently, Lunalytics is in a state where all updates should be stable but there might be a few issues. But, there are still a lot of features that are missing before the full release. 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/kanban/header.md: -------------------------------------------------------------------------------- 1 | # Headers, Body, and Authentication 2 | 3 | ## Headers 4 | 5 | - Leave as empty 6 | - Accepts JSON format 7 | 8 | ## Body 9 | 10 | - Currently only support JSON format 11 | 12 | ## Authentication 13 | 14 | - None 15 | - HTTP Basic Auth (Username and Password) 16 | - OAuth Credentials 17 | -------------------------------------------------------------------------------- /docs/kanban/notifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | sidebar: false 4 | title: Notifications 5 | --- 6 | 7 | # Notifications 8 | 9 | ## Platforms 10 | 11 | ### Discord 12 | 13 | ### Slack 14 | 15 | ### Teams 16 | 17 | ### Telegram 18 | 19 | ### Webhooks 20 | -------------------------------------------------------------------------------- /docs/public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/demo.gif -------------------------------------------------------------------------------- /docs/public/guides/Discord_Integration.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Discord_Integration.webp -------------------------------------------------------------------------------- /docs/public/guides/Discord_Webhook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Discord_Webhook.webp -------------------------------------------------------------------------------- /docs/public/guides/Lunalytics_Add_Monitor_Notification.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Lunalytics_Add_Monitor_Notification.webp -------------------------------------------------------------------------------- /docs/public/guides/Lunalytics_Create_Monitor.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Lunalytics_Create_Monitor.webp -------------------------------------------------------------------------------- /docs/public/guides/Lunalytics_Create_Notification.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Lunalytics_Create_Notification.webp -------------------------------------------------------------------------------- /docs/public/guides/Lunalytics_Discord_Create_Notification.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/guides/Lunalytics_Discord_Create_Notification.webp -------------------------------------------------------------------------------- /docs/public/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/header.png -------------------------------------------------------------------------------- /docs/public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/icon-192x192.png -------------------------------------------------------------------------------- /docs/public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/docs/public/icon-512x512.png -------------------------------------------------------------------------------- /docs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lunalytics Docs", 3 | "short_name": "Lunalytics Docs", 4 | "start_url": "/", 5 | "background_color": "#fff", 6 | "display": "standalone", 7 | "icons": [ 8 | { 9 | "src": "icon-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "icon-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import reactRefresh from 'eslint-plugin-react-refresh'; 3 | import globals from 'globals'; 4 | import path from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | import js from '@eslint/js'; 7 | import { FlatCompat } from '@eslint/eslintrc'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | { ignores: ['**/dist', '**/.eslintrc.js'] }, 19 | ...fixupConfigRules( 20 | compat.extends( 21 | 'eslint:recommended', 22 | 'plugin:react/recommended', 23 | 'plugin:react/jsx-runtime', 24 | 'plugin:react-hooks/recommended' 25 | ) 26 | ), 27 | { 28 | plugins: { 'react-refresh': reactRefresh }, 29 | languageOptions: { 30 | globals: { ...globals.browser, ...globals.node }, 31 | ecmaVersion: 'latest', 32 | sourceType: 'module', 33 | }, 34 | settings: { react: { version: '19' } }, 35 | rules: { 36 | 'react/prop-types': 'error', 37 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 38 | }, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Lunalytics 9 | 17 | 18 | 19 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/LogoWithName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/LogoWithName.png -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icon-512x512.png -------------------------------------------------------------------------------- /public/icons/Ape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Ape.png -------------------------------------------------------------------------------- /public/icons/Bear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Bear.png -------------------------------------------------------------------------------- /public/icons/Cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Cat.png -------------------------------------------------------------------------------- /public/icons/Dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Dog.png -------------------------------------------------------------------------------- /public/icons/Duck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Duck.png -------------------------------------------------------------------------------- /public/icons/Eagle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Eagle.png -------------------------------------------------------------------------------- /public/icons/Fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Fox.png -------------------------------------------------------------------------------- /public/icons/Gerbil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Gerbil.png -------------------------------------------------------------------------------- /public/icons/Hamster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Hamster.png -------------------------------------------------------------------------------- /public/icons/Hedgehog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Hedgehog.png -------------------------------------------------------------------------------- /public/icons/Koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Koala.png -------------------------------------------------------------------------------- /public/icons/Ostrich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Ostrich.png -------------------------------------------------------------------------------- /public/icons/Panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Panda.png -------------------------------------------------------------------------------- /public/icons/Rabbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Rabbit.png -------------------------------------------------------------------------------- /public/icons/Rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Rocket.png -------------------------------------------------------------------------------- /public/icons/Smart-Dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Smart-Dog.png -------------------------------------------------------------------------------- /public/icons/Tiger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/icons/Tiger.png -------------------------------------------------------------------------------- /public/logo/sqlite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lunalytics", 3 | "short_name": "Lunalytics", 4 | "start_url": "/", 5 | "background_color": "#fff", 6 | "display": "standalone", 7 | "icons": [ 8 | { 9 | "src": "icon-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "icon-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /public/meme/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KSJaay/Lunalytics/477d9bf307a084ae44fd95bc150468f0d63495c7/public/meme/cat.png -------------------------------------------------------------------------------- /public/notifications/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/notifications/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /scripts/migrate_manual.js: -------------------------------------------------------------------------------- 1 | import migrateDatabase from './migrate.js'; 2 | 3 | migrateDatabase() 4 | .then(() => { 5 | process.exit(0); 6 | }) 7 | .catch((error) => { 8 | console.error(error); 9 | process.exit(1); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/migrations/0-4-0.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import SQLite from '../../server/database/sqlite/setup.js'; 3 | import logger from '../../server/utils/logger.js'; 4 | 5 | const infomation = { 6 | title: 'Support for TCP pings', 7 | description: 8 | 'Adds support for TCP pings. This allows you to monitor TCP services such as databases, web servers, and more. This update also adds a new column to the monitor table to store the type of monitor, and port for tcp monitors.', 9 | version: '0.4.0', 10 | }; 11 | 12 | const migrate = async () => { 13 | const client = await SQLite.connect(); 14 | 15 | await client.schema.alterTable('monitor', (table) => { 16 | table.string('type').defaultTo('http'); 17 | table.integer('port').defaultTo(null); 18 | table.string('method').defaultTo(null).alter(); 19 | table.string('interval').defaultTo(30).alter(); 20 | table.string('retryInterval').defaultTo(30).alter(); 21 | table.string('requestTimeout').defaultTo(30).alter(); 22 | table.string('method').defaultTo(null).alter(); 23 | }); 24 | 25 | logger.info('Migrations', { message: '0.4.0 has been applied' }); 26 | return; 27 | }; 28 | 29 | export { infomation, migrate }; 30 | -------------------------------------------------------------------------------- /scripts/migrations/0-6-0.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import SQLite from '../../server/database/sqlite/setup.js'; 3 | import logger from '../../server/utils/logger.js'; 4 | 5 | const infomation = { 6 | title: 'Support for Notifications', 7 | description: 8 | 'Adds support for Notifications. This allows you to send notifications to users when a monitor goes down or up. This update also adds a page to the dashboard to view all notifications.', 9 | version: '0.6.0', 10 | breaking: true, 11 | }; 12 | 13 | const migrate = async () => { 14 | const client = await SQLite.connect(); 15 | 16 | // Add notifications object field to monitors table 17 | await client.schema.alterTable('monitor', (table) => { 18 | table.string('notificationId'); 19 | table.string('notificationType').defaultTo('All'); 20 | }); 21 | 22 | logger.info('Migrations', { message: '0.6.0 has been applied' }); 23 | return; 24 | }; 25 | 26 | export { infomation, migrate }; 27 | -------------------------------------------------------------------------------- /scripts/migrations/0-6-5.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import SQLite from '../../server/database/sqlite/setup.js'; 3 | import logger from '../../server/utils/logger.js'; 4 | 5 | const infomation = { 6 | title: 'Removes caching system', 7 | description: 8 | 'Removes caching system, and adds column to certificate table for the next check time.', 9 | version: '0.6.5', 10 | breaking: true, 11 | }; 12 | 13 | const migrate = async () => { 14 | const client = await SQLite.connect(); 15 | 16 | await client.schema.alterTable('certificate', (table) => { 17 | table.timestamp('nextCheck'); 18 | }); 19 | 20 | logger.info('Migrations', { message: '0.6.5 has been applied' }); 21 | return; 22 | }; 23 | 24 | export { infomation, migrate }; 25 | -------------------------------------------------------------------------------- /scripts/migrations/0-7-2.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import SQLite from '../../server/database/sqlite/setup.js'; 3 | import logger from '../../server/utils/logger.js'; 4 | 5 | const infomation = { 6 | title: 'Add pause for monitors', 7 | description: 8 | 'Allow monitors to be paused, this will stop checks for the monitor.', 9 | version: '0.7.2', 10 | }; 11 | 12 | const migrate = async () => { 13 | const client = await SQLite.connect(); 14 | 15 | await client.schema.alterTable('monitor', (table) => { 16 | table.boolean('paused').defaultTo(false); 17 | }); 18 | 19 | logger.info('Migrations', { message: '0.7.2 has been applied' }); 20 | return; 21 | }; 22 | 23 | export { infomation, migrate }; 24 | -------------------------------------------------------------------------------- /scripts/migrations/0-8-0.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import SQLite from '../../server/database/sqlite/setup.js'; 3 | import logger from '../../server/utils/logger.js'; 4 | 5 | const infomation = { 6 | title: 'Add pause for monitors', 7 | description: 8 | 'Allow monitors to be paused, this will stop checks for the monitor.', 9 | version: '0.7.2', 10 | }; 11 | 12 | const migrate = async () => { 13 | const client = await SQLite.connect(); 14 | 15 | await client.schema.alterTable('monitor', (table) => { 16 | table.datetime('createdAt'); 17 | }); 18 | 19 | const monitors = await client('monitor').select(); 20 | 21 | for (const monitor of monitors) { 22 | const oldestHeartbeat = await client('heartbeat') 23 | .where('monitorId', monitor.monitorId) 24 | .orderBy('date', 'asc') 25 | .first(); 26 | 27 | if (oldestHeartbeat) { 28 | await client('monitor') 29 | .where('monitorId', monitor.monitorId) 30 | .update({ createdAt: oldestHeartbeat.date }); 31 | } 32 | } 33 | 34 | logger.info('Migrations', { message: '0.8.0 has been applied' }); 35 | return; 36 | }; 37 | 38 | export { infomation, migrate }; 39 | -------------------------------------------------------------------------------- /scripts/migrations/index.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import { migrate as migrateTcpUpdate } from './0-4-0.js'; 3 | import { migrate as migrateNotifications } from './0-6-0.js'; 4 | import { migrate as migrateCache } from './0-6-5.js'; 5 | import { migrate as migratePostgres } from './0-7-0.js'; 6 | import { migrate as migratePause } from './0-7-2.js'; 7 | import { migrate as migrateStatus } from './0-8-0.js'; 8 | 9 | const migrationList = { 10 | '0.4.0': migrateTcpUpdate, 11 | '0.6.0': migrateNotifications, 12 | '0.6.5': migrateCache, 13 | '0.7.0': migratePostgres, 14 | '0.7.2': migratePause, 15 | '0.8.0': migrateStatus, 16 | }; 17 | 18 | export default migrationList; 19 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // import local files 6 | import logger from '../server/utils/logger.js'; 7 | 8 | const configExists = () => { 9 | const configPath = path.join(process.cwd(), 'data', 'config.json'); 10 | return fs.existsSync(configPath); 11 | }; 12 | 13 | if (configExists()) { 14 | logger.notice('SETUP', { 15 | message: 16 | 'Configuration file already exists. Please manually edit to overwrite or delete the file.', 17 | }); 18 | process.exit(0); 19 | } 20 | 21 | try { 22 | logger.info('SETUP', { message: 'Creating config file...' }); 23 | // create data directory if it does not exist 24 | if (!fs.existsSync(path.join(process.cwd(), 'data'))) { 25 | fs.mkdirSync(path.join(process.cwd(), 'data')); 26 | } 27 | 28 | // create config.json file 29 | const configPath = path.join(process.cwd(), 'data', 'config.json'); 30 | 31 | fs.writeFileSync(configPath, JSON.stringify({}, null, 2)); 32 | logger.info('SETUP', { message: 'Successfully created config file.' }); 33 | 34 | process.exit(0); 35 | } catch (error) { 36 | logger.error('SETUP', { 37 | message: 'Unable to setup application. Please try again.', 38 | error: error.message, 39 | stack: error.stack, 40 | }); 41 | 42 | process.exit(1); 43 | } 44 | -------------------------------------------------------------------------------- /server/class/certificate.js: -------------------------------------------------------------------------------- 1 | const parseJson = (str) => { 2 | try { 3 | return JSON.parse(str); 4 | } catch { 5 | return ''; 6 | } 7 | }; 8 | 9 | const cleanCertificate = (certificate) => ({ 10 | isValid: certificate.isValid == '1', 11 | issuer: parseJson(certificate.issuer), 12 | validFrom: certificate.validFrom, 13 | validTill: certificate.validTill, 14 | validOn: parseJson(certificate.validOn), 15 | daysRemaining: certificate.daysRemaining, 16 | nextCheck: certificate.nextCheck, 17 | }); 18 | 19 | export default cleanCertificate; 20 | -------------------------------------------------------------------------------- /server/class/notification.js: -------------------------------------------------------------------------------- 1 | const parseJson = (str) => { 2 | try { 3 | return JSON.parse(str); 4 | } catch { 5 | return str; 6 | } 7 | }; 8 | 9 | const stringifyJson = (obj) => { 10 | try { 11 | return JSON.stringify(obj); 12 | } catch { 13 | return null; 14 | } 15 | }; 16 | 17 | export const cleanNotification = (notification) => ({ 18 | id: notification.id, 19 | platform: notification.platform, 20 | messageType: notification.messageType, 21 | token: notification.token, 22 | email: notification.email, 23 | friendlyName: notification.friendlyName, 24 | isEnabled: notification.isEnabled == '1', 25 | data: 26 | typeof notification.data === 'string' 27 | ? parseJson(notification.data) 28 | : notification.data, 29 | }); 30 | 31 | export const stringifyNotification = (notification) => ({ 32 | id: notification.id, 33 | platform: notification.platform, 34 | messageType: notification.messageType, 35 | token: notification.token, 36 | email: notification.email, 37 | friendlyName: notification.friendlyName, 38 | isEnabled: notification.isEnabled == '1', 39 | data: stringifyJson(notification.data), 40 | }); 41 | -------------------------------------------------------------------------------- /server/class/status.js: -------------------------------------------------------------------------------- 1 | import { defaultStatusValues } from '../../shared/constants/status.js'; 2 | 3 | const parseJson = (obj, isArray = false) => { 4 | try { 5 | return JSON.parse(obj); 6 | } catch { 7 | return isArray ? [] : {}; 8 | } 9 | }; 10 | 11 | export const cleanStatusPage = (status) => ({ 12 | id: status.id, 13 | statusId: status.statusId, 14 | statusUrl: status.statusUrl, 15 | settings: { ...defaultStatusValues, ...parseJson(status.settings) }, 16 | layout: parseJson(status.layout, true), 17 | email: status.email, 18 | createdAt: status.createdAt, 19 | }); 20 | 21 | export const cleanStatusPageWithMonitors = (status) => ({ 22 | settings: { ...defaultStatusValues, ...parseJson(status.settings) }, 23 | layout: parseJson(status.layout, true), 24 | }); 25 | 26 | export const cleanStatusApiResponse = (data) => ({ 27 | id: data.id, 28 | statusId: data.statusId, 29 | statusUrl: data.statusUrl, 30 | settings: data.settings, 31 | layout: data.layout, 32 | monitors: data.monitors, 33 | incidents: data.incidents, 34 | heartbeats: data.heartbeats, 35 | }); 36 | -------------------------------------------------------------------------------- /server/database/queries/api.js: -------------------------------------------------------------------------------- 1 | import SQLite from '../sqlite/setup.js'; 2 | import { nanoid } from 'nanoid'; 3 | 4 | export const apiTokenExists = async (token) => { 5 | return SQLite.client('api_token').where({ token }).first(); 6 | }; 7 | 8 | const getUniqueToken = async () => { 9 | let token = nanoid(64); 10 | 11 | while (await SQLite.client('api_token').where({ token }).first()) { 12 | token = nanoid(64); 13 | } 14 | 15 | return token; 16 | }; 17 | 18 | export const apiTokenCreate = async (email, permission) => { 19 | const token = await getUniqueToken(); 20 | 21 | await SQLite.client('api_token').insert({ 22 | token, 23 | permission, 24 | email, 25 | createdAt: new Date().toISOString(), 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /server/database/queries/certificate.js: -------------------------------------------------------------------------------- 1 | import cleanCertificate from '../../class/certificate.js'; 2 | import SQLite from '../sqlite/setup.js'; 3 | 4 | export const fetchCertificate = async (monitorId) => { 5 | const certificate = await SQLite.client('certificate') 6 | .where({ monitorId }) 7 | .first(); 8 | 9 | if (!certificate) { 10 | return { isValid: false }; 11 | } 12 | 13 | return cleanCertificate(certificate); 14 | }; 15 | 16 | export const updateCertificate = async (monitorId, certificate) => { 17 | const cert = await SQLite.client('certificate').where({ monitorId }).first(); 18 | 19 | if (!cert) { 20 | await SQLite.client('certificate').insert({ monitorId, ...certificate }); 21 | } else { 22 | await SQLite.client('certificate').where({ monitorId }).update(certificate); 23 | } 24 | 25 | return true; 26 | }; 27 | 28 | export const deleteCertificate = async (monitorId) => { 29 | await SQLite.client('certificate').where({ monitorId }).del(); 30 | }; 31 | -------------------------------------------------------------------------------- /server/database/queries/session.js: -------------------------------------------------------------------------------- 1 | import SQLite from '../sqlite/setup.js'; 2 | import { nanoid } from 'nanoid'; 3 | 4 | const getUniqueSessionId = async () => { 5 | let sessionId = nanoid(64); 6 | 7 | while (await userSessionExists(sessionId)) { 8 | sessionId = await getUniqueSessionId(); 9 | } 10 | 11 | return sessionId; 12 | }; 13 | 14 | export const createUserSession = async (email, device, data) => { 15 | const sessionId = await getUniqueSessionId(); 16 | 17 | await SQLite.client('user_session').insert({ 18 | email, 19 | sessionId, 20 | device, 21 | data, 22 | createdAt: new Date().toISOString(), 23 | }); 24 | 25 | return sessionId; 26 | }; 27 | 28 | export const userSessionExists = async (sessionId) => { 29 | return SQLite.client('user_session').where({ sessionId }).first(); 30 | }; 31 | 32 | export const deleteUserSession = async (sessionId) => { 33 | return SQLite.client('user_session').where({ sessionId }).del(); 34 | }; 35 | -------------------------------------------------------------------------------- /server/middleware/auth/emailExists.js: -------------------------------------------------------------------------------- 1 | import { emailExists } from '../../database/queries/user.js'; 2 | 3 | const emailExistsMiddleware = async (request, response) => { 4 | const { email } = request.body; 5 | if (!email) return response.status(400).send('No email provided'); 6 | 7 | const user = await emailExists(email); 8 | if (!user) return response.send(false); 9 | 10 | return response.send(true); 11 | }; 12 | 13 | export default emailExistsMiddleware; 14 | -------------------------------------------------------------------------------- /server/middleware/auth/index.js: -------------------------------------------------------------------------------- 1 | import login from './login.js'; 2 | import logout from './logout.js'; 3 | import register from './register.js'; 4 | import setup from './setup.js'; 5 | 6 | export { login, logout, register, setup }; 7 | -------------------------------------------------------------------------------- /server/middleware/auth/logout.js: -------------------------------------------------------------------------------- 1 | import { deleteCookie } from '../../../shared/utils/cookies.js'; 2 | import { handleError } from '../../utils/errors.js'; 3 | import { createURL } from '../../../shared/utils/url.js'; 4 | 5 | const logout = (request, response) => { 6 | try { 7 | deleteCookie(response, 'session_token'); 8 | 9 | return response.redirect(createURL('/login')); 10 | } catch (error) { 11 | return handleError(error, response); 12 | } 13 | }; 14 | 15 | export default logout; 16 | -------------------------------------------------------------------------------- /server/middleware/declineApiAccess.js: -------------------------------------------------------------------------------- 1 | export const declineApiAccess = async (request, response, next) => { 2 | if (request.user.isApiToken) { 3 | return response.sendStatus(401); 4 | } 5 | 6 | return next(); 7 | }; 8 | -------------------------------------------------------------------------------- /server/middleware/demo.js: -------------------------------------------------------------------------------- 1 | import { getDemoUser, resetDemoUser } from '../database/queries/user.js'; 2 | import { setDemoCookie } from '../../shared/utils/cookies.js'; 3 | import config from '../utils/config.js'; 4 | 5 | const isDemoMode = config.get('isDemo'); 6 | 7 | const isDemo = async (request, response, next) => { 8 | const { session_token } = request.cookies; 9 | 10 | if (process.env.NODE_ENV === 'production' && isDemoMode && !session_token) { 11 | if ( 12 | !request.url.startsWith('/register') && 13 | !request.url.startsWith('/login') 14 | ) { 15 | const cookie = await getDemoUser(); 16 | setDemoCookie(response, 'session_token', cookie); 17 | request.cookies['session_token'] = cookie; 18 | } 19 | } 20 | 21 | if (process.env.NODE_ENV === 'production' && isDemoMode) { 22 | await resetDemoUser(); 23 | } 24 | 25 | return next(); 26 | }; 27 | 28 | export default isDemo; 29 | -------------------------------------------------------------------------------- /server/middleware/monitor/delete.js: -------------------------------------------------------------------------------- 1 | // import local files 2 | import { handleError } from '../../utils/errors.js'; 3 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 4 | import { deleteCertificate } from '../../database/queries/certificate.js'; 5 | import { deleteHeartbeats } from '../../database/queries/heartbeat.js'; 6 | import { deleteMonitor } from '../../database/queries/monitor.js'; 7 | 8 | const monitorDelete = async (request, response) => { 9 | try { 10 | const { monitorId } = request.query; 11 | 12 | if (!monitorId) { 13 | throw new UnprocessableError('No monitorId provided'); 14 | } 15 | 16 | await deleteMonitor(monitorId); 17 | await deleteHeartbeats(monitorId); 18 | await deleteCertificate(monitorId); 19 | 20 | return response.sendStatus(200); 21 | } catch (error) { 22 | return handleError(error, response); 23 | } 24 | }; 25 | 26 | export default monitorDelete; 27 | -------------------------------------------------------------------------------- /server/middleware/monitor/id.js: -------------------------------------------------------------------------------- 1 | import { cleanMonitor } from '../../class/monitor.js'; 2 | import { handleError } from '../../utils/errors.js'; 3 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 4 | import { fetchMonitor } from '../../database/queries/monitor.js'; 5 | import { fetchCertificate } from '../../database/queries/certificate.js'; 6 | import { fetchHeartbeats } from '../../database/queries/heartbeat.js'; 7 | 8 | const fetchMonitorUsingId = async (request, response) => { 9 | try { 10 | const { monitorId } = request.query; 11 | 12 | if (!monitorId) { 13 | throw new UnprocessableError('No monitorId provided'); 14 | } 15 | 16 | const data = await fetchMonitor(monitorId); 17 | 18 | if (!data) { 19 | return response.status(404).json({ error: 'Monitor not found' }); 20 | } 21 | 22 | const heartbeats = await fetchHeartbeats(data.monitorId); 23 | const cert = await fetchCertificate(data.monitorId); 24 | 25 | const monitor = cleanMonitor({ 26 | ...data, 27 | heartbeats, 28 | cert, 29 | }); 30 | 31 | return response.json(monitor); 32 | } catch (error) { 33 | handleError(error, response); 34 | } 35 | }; 36 | 37 | export default fetchMonitorUsingId; 38 | -------------------------------------------------------------------------------- /server/middleware/monitor/pause.js: -------------------------------------------------------------------------------- 1 | import cache from '../../cache/index.js'; 2 | import { pauseMonitor } from '../../database/queries/monitor.js'; 3 | import { handleError } from '../../utils/errors.js'; 4 | 5 | const isTruthy = (value) => value == true || value == 'true'; 6 | const isFalsy = (value) => value == false || value == 'false'; 7 | 8 | const monitorPause = async (request, response) => { 9 | try { 10 | const { monitorId, pause } = request.body; 11 | 12 | if (!monitorId) { 13 | throw new Error('No monitorId provided'); 14 | } 15 | 16 | if (!isTruthy(pause) && !isFalsy(pause)) { 17 | throw new Error('Pause should be a boolean value'); 18 | } 19 | 20 | await pauseMonitor(monitorId, isTruthy(pause)); 21 | 22 | if (isTruthy(pause)) { 23 | cache.removeMonitor(monitorId); 24 | } else { 25 | await cache.checkStatus(monitorId); 26 | } 27 | 28 | return response.sendStatus(200); 29 | } catch (error) { 30 | handleError(error, response); 31 | } 32 | }; 33 | 34 | export default monitorPause; 35 | -------------------------------------------------------------------------------- /server/middleware/notifications/create.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 3 | import NotificationValidators from '../../../shared/validators/notifications/index.js'; 4 | import { 5 | createNotification, 6 | fetchNotificationUniqueId, 7 | } from '../../database/queries/notification.js'; 8 | 9 | const NotificationCreateMiddleware = async (request, response) => { 10 | const notification = request.body; 11 | 12 | try { 13 | const validator = NotificationValidators[notification?.platform]; 14 | 15 | if (!validator) { 16 | throw new UnprocessableError('Invalid Notification Platform'); 17 | } 18 | 19 | const result = validator({ ...notification, ...notification.data }); 20 | 21 | const { user } = response.locals; 22 | 23 | const uniqueId = await fetchNotificationUniqueId(); 24 | const query = await createNotification({ 25 | ...result, 26 | email: user.email, 27 | id: uniqueId, 28 | isEnabled: true, 29 | }); 30 | 31 | return response.status(201).send(query); 32 | } catch (error) { 33 | handleError(error, response); 34 | } 35 | }; 36 | 37 | export default NotificationCreateMiddleware; 38 | -------------------------------------------------------------------------------- /server/middleware/notifications/delete.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 3 | import { deleteNotification } from '../../database/queries/notification.js'; 4 | 5 | const NotificationDeleteMiddleware = async (request, response) => { 6 | const { notificationId } = request.query; 7 | 8 | if (!notificationId) { 9 | throw new UnprocessableError('No notificationId provided'); 10 | } 11 | 12 | try { 13 | await deleteNotification(notificationId); 14 | return response.status(200).send('Notification deleted'); 15 | } catch (error) { 16 | handleError(error, response); 17 | } 18 | }; 19 | 20 | export default NotificationDeleteMiddleware; 21 | -------------------------------------------------------------------------------- /server/middleware/notifications/disable.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 3 | import { toggleNotification } from '../../database/queries/notification.js'; 4 | 5 | const NotificationToggleMiddleware = async (request, response) => { 6 | const { notificationId, isEnabled } = request.query; 7 | 8 | if (!notificationId) { 9 | throw new UnprocessableError('No notificationId provided'); 10 | } 11 | 12 | if (isEnabled !== 'true' && isEnabled !== 'false') { 13 | throw new UnprocessableError('isEnabled is not a boolean'); 14 | } 15 | 16 | try { 17 | await toggleNotification(notificationId, isEnabled === 'true'); 18 | return response.sendStatus(200); 19 | } catch (error) { 20 | handleError(error, response); 21 | } 22 | }; 23 | 24 | export default NotificationToggleMiddleware; 25 | -------------------------------------------------------------------------------- /server/middleware/notifications/edit.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { UnprocessableError } from '../../../shared/utils/errors.js'; 3 | import NotificationValidators from '../../../shared/validators/notifications/index.js'; 4 | import { editNotification } from '../../database/queries/notification.js'; 5 | 6 | const NotificationEditMiddleware = async (request, response) => { 7 | const notification = request.body; 8 | 9 | try { 10 | const validator = NotificationValidators[notification?.platform]; 11 | 12 | if (!validator) { 13 | throw new UnprocessableError('Invalid Notification Platform'); 14 | } 15 | 16 | const result = validator({ ...notification, ...notification.data }); 17 | 18 | const query = await editNotification({ 19 | ...result, 20 | id: notification.id, 21 | email: notification.email, 22 | isEnabled: notification.isEnabled, 23 | }); 24 | 25 | return response.json(query); 26 | } catch (error) { 27 | handleError(error, response); 28 | } 29 | }; 30 | 31 | export default NotificationEditMiddleware; 32 | -------------------------------------------------------------------------------- /server/middleware/notifications/getAll.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { fetchNotifications } from '../../database/queries/notification.js'; 3 | 4 | const NotificationGetAllMiddleware = async (request, response) => { 5 | try { 6 | const notifications = await fetchNotifications(); 7 | 8 | return response.json(notifications); 9 | } catch (error) { 10 | handleError(error, response); 11 | } 12 | }; 13 | 14 | export default NotificationGetAllMiddleware; 15 | -------------------------------------------------------------------------------- /server/middleware/notifications/getUsingId.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | import { fetchNotificationById } from '../../database/queries/notification.js'; 3 | import logger from '../../utils/logger.js'; 4 | 5 | const NotificationGetUsingIdMiddleware = async (request, response) => { 6 | const { notificationId } = request.query; 7 | 8 | try { 9 | const notification = await fetchNotificationById(notificationId); 10 | 11 | if (!notification) { 12 | logger.error('Notification - getById', { 13 | notificationId, 14 | message: 'Notification does not exist', 15 | }); 16 | 17 | return response.status(404).send({ 18 | message: 'Notification not found', 19 | }); 20 | } 21 | 22 | return response.status(200).send(notification); 23 | } catch (error) { 24 | handleError(error, response); 25 | } 26 | }; 27 | 28 | export default NotificationGetUsingIdMiddleware; 29 | -------------------------------------------------------------------------------- /server/middleware/status/delete.js: -------------------------------------------------------------------------------- 1 | import statusCache from '../../cache/status.js'; 2 | import { deleteStatusPage } from '../../database/queries/status.js'; 3 | import { handleError } from '../../utils/errors.js'; 4 | 5 | const deleteStatusPageMiddleware = async (request, response) => { 6 | const { statusPageId } = request.body; 7 | 8 | try { 9 | if (!statusPageId) { 10 | throw new Error('Status page id is required.'); 11 | } 12 | 13 | await deleteStatusPage(statusPageId); 14 | statusCache.deleteStatusPage(statusPageId); 15 | 16 | response.status(200).send({ 17 | message: 'Status page deleted successfully!', 18 | }); 19 | } catch (error) { 20 | if (!response.headersSent) { 21 | if (error instanceof Error) { 22 | response.status(400).send({ 23 | message: error.message, 24 | }); 25 | } 26 | } 27 | 28 | handleError(error, response); 29 | } 30 | }; 31 | 32 | export default deleteStatusPageMiddleware; 33 | -------------------------------------------------------------------------------- /server/middleware/status/getAll.js: -------------------------------------------------------------------------------- 1 | import { fetchAllStatusPages } from '../../database/queries/status.js'; 2 | import { handleError } from '../../utils/errors.js'; 3 | 4 | const getAllStatusPagesMiddleware = async (request, response) => { 5 | try { 6 | const query = await fetchAllStatusPages(); 7 | 8 | return response.json(query); 9 | } catch (error) { 10 | handleError(error, response); 11 | } 12 | }; 13 | 14 | export default getAllStatusPagesMiddleware; 15 | -------------------------------------------------------------------------------- /server/middleware/status/getUsingId.js: -------------------------------------------------------------------------------- 1 | import { cleanStatusPage } from '../../class/status.js'; 2 | import { fetchStatusPageUsingUrl } from '../../database/queries/status.js'; 3 | import { handleError } from '../../utils/errors.js'; 4 | 5 | const getUsingIdMiddleware = async (request, response) => { 6 | try { 7 | const { statusPageId } = request.query; 8 | 9 | if (!statusPageId) { 10 | return response.sendStatus(404); 11 | } 12 | 13 | const query = await fetchStatusPageUsingUrl(statusPageId); 14 | 15 | if (!query) { 16 | return response.sendStatus(404); 17 | } 18 | 19 | return response.json(cleanStatusPage(query)); 20 | } catch (error) { 21 | handleError(error, response); 22 | } 23 | }; 24 | 25 | export default getUsingIdMiddleware; 26 | -------------------------------------------------------------------------------- /server/middleware/user/access/approveUser.js: -------------------------------------------------------------------------------- 1 | import { approveAccess } from '../../../database/queries/user.js'; 2 | import { handleError } from '../../../utils/errors.js'; 3 | 4 | const accessApproveMiddleware = async (request, response) => { 5 | try { 6 | const { email } = request.body; 7 | 8 | if (!email) { 9 | return response.sendStatus(400); 10 | } 11 | 12 | await approveAccess(email); 13 | 14 | return response.sendStatus(200); 15 | } catch (error) { 16 | handleError(error, response); 17 | } 18 | }; 19 | 20 | export default accessApproveMiddleware; 21 | -------------------------------------------------------------------------------- /server/middleware/user/access/declineUser.js: -------------------------------------------------------------------------------- 1 | import { declineAccess } from '../../../database/queries/user.js'; 2 | import { handleError } from '../../../utils/errors.js'; 3 | 4 | const accessDeclineMiddleware = async (request, response) => { 5 | try { 6 | const { email } = request.body; 7 | 8 | if (!email) { 9 | return response.sendStatus(400); 10 | } 11 | 12 | await declineAccess(email); 13 | 14 | return response.sendStatus(200); 15 | } catch (error) { 16 | handleError(error, response); 17 | } 18 | }; 19 | 20 | export default accessDeclineMiddleware; 21 | -------------------------------------------------------------------------------- /server/middleware/user/access/removeUser.js: -------------------------------------------------------------------------------- 1 | import { declineAccess } from '../../../database/queries/user.js'; 2 | import { handleError } from '../../../utils/errors.js'; 3 | 4 | const accessRemoveMiddleware = async (request, response) => { 5 | try { 6 | const { email } = request.body; 7 | 8 | if (!email) { 9 | return response.sendStatus(400); 10 | } 11 | 12 | await declineAccess(email); 13 | 14 | return response.sendStatus(200); 15 | } catch (error) { 16 | handleError(error, response); 17 | } 18 | }; 19 | 20 | export default accessRemoveMiddleware; 21 | -------------------------------------------------------------------------------- /server/middleware/user/deleteAccount.js: -------------------------------------------------------------------------------- 1 | import { declineAccess, emailIsOwner } from '../../database/queries/user.js'; 2 | import { handleError } from '../../utils/errors.js'; 3 | 4 | const deleteAccountMiddleware = async (request, response) => { 5 | try { 6 | const { user } = response.locals; 7 | 8 | const userIsOwner = await emailIsOwner(user.email); 9 | 10 | if (userIsOwner.permission === 1) { 11 | return response 12 | .status(403) 13 | .send('Please transfer ownership before deleting your account.'); 14 | } 15 | 16 | await declineAccess(user.email); 17 | 18 | return response.sendStatus(200); 19 | } catch (error) { 20 | handleError(error, response); 21 | } 22 | }; 23 | 24 | export default deleteAccountMiddleware; 25 | -------------------------------------------------------------------------------- /server/middleware/user/exists.js: -------------------------------------------------------------------------------- 1 | import { emailExists } from '../../database/queries/user.js'; 2 | import { handleError } from '../../utils/errors.js'; 3 | 4 | const userExistsMiddleware = async (request, response) => { 5 | try { 6 | const { email } = request.body; 7 | if (!email) return response.status(400).send('No email provided'); 8 | 9 | const user = await emailExists(email); 10 | if (!user) return response.send(false); 11 | 12 | return response.send(true); 13 | } catch (error) { 14 | handleError(error, response); 15 | } 16 | }; 17 | 18 | export default userExistsMiddleware; 19 | -------------------------------------------------------------------------------- /server/middleware/user/hasPermission.js: -------------------------------------------------------------------------------- 1 | import Role from '../../../shared/permissions/role.js'; 2 | 3 | export const hasRequiredPermission = 4 | (requiredPermission) => (request, response, next) => { 5 | const { 6 | user: { permission }, 7 | } = response.locals; 8 | if (!permission) return response.sendStatus(401); 9 | 10 | const role = new Role('user', permission); 11 | 12 | if (!role.hasPermission(requiredPermission)) { 13 | return response.sendStatus(401); 14 | } 15 | 16 | return next(); 17 | }; 18 | -------------------------------------------------------------------------------- /server/middleware/user/monitors.js: -------------------------------------------------------------------------------- 1 | import { cleanMonitor } from '../../class/monitor.js'; 2 | import { fetchCertificate } from '../../database/queries/certificate.js'; 3 | import { 4 | fetchHeartbeats, 5 | fetchHourlyHeartbeats, 6 | } from '../../database/queries/heartbeat.js'; 7 | import { fetchMonitors } from '../../database/queries/monitor.js'; 8 | import { handleError } from '../../utils/errors.js'; 9 | 10 | const userMonitorsMiddleware = async (request, response) => { 11 | try { 12 | const monitors = await fetchMonitors(); 13 | const query = []; 14 | 15 | for (const monitor of monitors) { 16 | const heartbeats = await fetchHeartbeats(monitor.monitorId, 12); 17 | monitor.heartbeats = heartbeats; 18 | 19 | monitor.cert = { isValid: false }; 20 | 21 | if (monitor.type === 'http') { 22 | const cert = await fetchCertificate(monitor.monitorId); 23 | monitor.cert = cert; 24 | } 25 | 26 | const filters = await fetchHourlyHeartbeats(monitor.monitorId, 2); 27 | monitor.showFilters = filters.length === 2; 28 | 29 | query.push(cleanMonitor(monitor)); 30 | } 31 | 32 | return response.send(query); 33 | } catch (error) { 34 | handleError(error, response); 35 | } 36 | }; 37 | 38 | export default userMonitorsMiddleware; 39 | -------------------------------------------------------------------------------- /server/middleware/user/team/members.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBits } from '../../../../shared/permissions/bitFlags.js'; 2 | import Role from '../../../../shared/permissions/role.js'; 3 | import { fetchMembers } from '../../../database/queries/user.js'; 4 | import { handleError } from '../../../utils/errors.js'; 5 | 6 | const teamMembersListMiddleware = async (request, response) => { 7 | try { 8 | const { user } = response.locals; 9 | 10 | const role = new Role('user', user.permission); 11 | const userHasManageTeam = role.hasPermission(PermissionsBits.MANAGE_TEAM); 12 | 13 | const members = await fetchMembers(userHasManageTeam); 14 | 15 | return response.send(members); 16 | } catch (error) { 17 | handleError(error, response); 18 | } 19 | }; 20 | 21 | export default teamMembersListMiddleware; 22 | -------------------------------------------------------------------------------- /server/middleware/user/transferOwnership.js: -------------------------------------------------------------------------------- 1 | import { 2 | emailExists, 3 | emailIsOwner, 4 | transferOwnership, 5 | } from '../../database/queries/user.js'; 6 | import { handleError } from '../../utils/errors.js'; 7 | 8 | const transferOwnershipMiddleware = async (request, response) => { 9 | try { 10 | const { email } = request.body; 11 | 12 | if (!email) { 13 | return response.sendStatus(400); 14 | } 15 | 16 | const { user } = response.locals; 17 | 18 | const userIsOwner = await emailIsOwner(user.email); 19 | 20 | if (userIsOwner.permission !== 1) { 21 | return response.sendStatus(401); 22 | } 23 | 24 | const newOwnerExists = await emailExists(email); 25 | 26 | if (!newOwnerExists) { 27 | return response.sendStatus(400); 28 | } 29 | 30 | await transferOwnership(user.email, email); 31 | 32 | return response.sendStatus(200); 33 | } catch (error) { 34 | handleError(error, response); 35 | } 36 | }; 37 | 38 | export default transferOwnershipMiddleware; 39 | -------------------------------------------------------------------------------- /server/middleware/user/update/avatar.js: -------------------------------------------------------------------------------- 1 | import { updateUserAvatar } from '../../../database/queries/user.js'; 2 | import { handleError } from '../../../utils/errors.js'; 3 | import validators from '../../../../shared/validators/index.js'; 4 | 5 | const userUpdateAvatar = async (request, response) => { 6 | try { 7 | const { user } = response.locals; 8 | 9 | const { avatar } = request.body; 10 | 11 | if (avatar !== null && (!avatar || user.avatar === avatar)) { 12 | return response.sendStatus(200); 13 | } 14 | 15 | const isInvalidAvatar = validators.user.isAvatar(avatar); 16 | 17 | if (isInvalidAvatar) { 18 | return response.status(400).send(isInvalidAvatar); 19 | } 20 | 21 | await updateUserAvatar(user.email, avatar); 22 | 23 | return response.sendStatus(200); 24 | } catch (error) { 25 | handleError(error, response); 26 | } 27 | }; 28 | 29 | export default userUpdateAvatar; 30 | -------------------------------------------------------------------------------- /server/middleware/user/update/password.js: -------------------------------------------------------------------------------- 1 | import { 2 | getUserPasswordUsingEmail, 3 | updateUserPassword, 4 | } from '../../../database/queries/user.js'; 5 | import { handleError } from '../../../utils/errors.js'; 6 | import { verifyPassword } from '../../../utils/hashPassword.js'; 7 | import validators from '../../../../shared/validators/index.js'; 8 | 9 | const userUpdatePassword = async (request, response) => { 10 | try { 11 | const { user } = response.locals; 12 | 13 | const password = await getUserPasswordUsingEmail(user.email); 14 | 15 | const { currentPassword, newPassword } = request.body; 16 | 17 | const passwordMatches = verifyPassword(currentPassword, password); 18 | 19 | if (!passwordMatches) { 20 | return response.status(401).json({ 21 | current: 'Password does not match your current password', 22 | }); 23 | } 24 | 25 | const isInvalidPassword = validators.auth.password(newPassword); 26 | 27 | if (isInvalidPassword) { 28 | return response.status(400).send(isInvalidPassword); 29 | } 30 | 31 | await updateUserPassword(user.email, newPassword); 32 | 33 | return response.sendStatus(200); 34 | } catch (error) { 35 | handleError(error, response); 36 | } 37 | }; 38 | 39 | export default userUpdatePassword; 40 | -------------------------------------------------------------------------------- /server/middleware/user/update/username.js: -------------------------------------------------------------------------------- 1 | import { updateUserDisplayname } from '../../../database/queries/user.js'; 2 | import { handleError } from '../../../utils/errors.js'; 3 | import validators from '../../../../shared/validators/index.js'; 4 | 5 | const userUpdateUsername = async (request, response) => { 6 | try { 7 | const { user } = response.locals; 8 | const { displayName } = request.body; 9 | 10 | if (!displayName || user.displayName === displayName) { 11 | return response.sendStatus(200); 12 | } 13 | 14 | const isInvalidUsername = validators.auth.username(displayName); 15 | 16 | if (isInvalidUsername) { 17 | return response.status(400).send(isInvalidUsername); 18 | } 19 | 20 | await updateUserDisplayname(user.email, displayName); 21 | 22 | return response.sendStatus(200); 23 | } catch (error) { 24 | handleError(error, response); 25 | } 26 | }; 27 | 28 | export default userUpdateUsername; 29 | -------------------------------------------------------------------------------- /server/middleware/user/user.js: -------------------------------------------------------------------------------- 1 | import { handleError } from '../../utils/errors.js'; 2 | 3 | const fetchUserMiddleware = async (request, response) => { 4 | try { 5 | const { user } = response.locals; 6 | 7 | return response.send(user); 8 | } catch (error) { 9 | handleError(error, response); 10 | } 11 | }; 12 | 13 | export default fetchUserMiddleware; 14 | -------------------------------------------------------------------------------- /server/notifications/base.js: -------------------------------------------------------------------------------- 1 | const parseErrorData = (data) => { 2 | try { 3 | JSON.stringify(data); 4 | } catch { 5 | return data; 6 | } 7 | }; 8 | 9 | class NotificationBase { 10 | name = undefined; 11 | success = 'Sent Successfully!'; 12 | 13 | /** 14 | * Send a notification 15 | * @param {Object} notification Notification to send 16 | * @param {object} monitor Monitor details 17 | * @param {object} heartbeat Heartbeat details 18 | * @returns {Promise} Return successful message 19 | * @throws Throws error about you being a dummy :) 20 | */ 21 | 22 | // eslint-disable-next-line no-unused-vars 23 | async send(notification, monitor, heartbeat) { 24 | throw new Error('Override this function dummy!'); 25 | } 26 | 27 | // eslint-disable-next-line no-unused-vars 28 | async sendRecovery(notification, monitor, heartbeat) { 29 | throw new Error('Override this function dummy!'); 30 | } 31 | 32 | handleError(error) { 33 | let info = 'Error: ' + error + '\n'; 34 | 35 | if (error?.response?.data) { 36 | info += parseErrorData(error.response.data); 37 | } 38 | 39 | throw new Error(info); 40 | } 41 | } 42 | 43 | export default NotificationBase; 44 | -------------------------------------------------------------------------------- /server/notifications/discord.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import NotificationReplacers from '../../shared/notifications/replacers/notification.js'; 3 | import NotificationBase from './base.js'; 4 | import { DiscordTemplateMessages } from '../../shared/notifications/discord.js'; 5 | 6 | class Discord extends NotificationBase { 7 | name = 'Discord'; 8 | 9 | async send(notification, monitor, heartbeat) { 10 | try { 11 | const template = 12 | DiscordTemplateMessages[notification.messageType] || 13 | notification.payload; 14 | 15 | const embed = NotificationReplacers(template, monitor, heartbeat); 16 | 17 | await axios.post(notification.token, { ...embed }); 18 | return this.success; 19 | } catch (error) { 20 | this.handleError(error); 21 | } 22 | } 23 | 24 | async sendRecovery(notification, monitor, heartbeat) { 25 | try { 26 | const template = DiscordTemplateMessages.recovery; 27 | 28 | const embed = NotificationReplacers(template, monitor, heartbeat); 29 | 30 | await axios.post(notification.token, { ...embed }); 31 | return this.success; 32 | } catch (error) { 33 | this.handleError(error); 34 | } 35 | } 36 | } 37 | 38 | export default Discord; 39 | -------------------------------------------------------------------------------- /server/notifications/index.js: -------------------------------------------------------------------------------- 1 | import Discord from './discord.js'; 2 | import Telegram from './telegram.js'; 3 | import Slack from './slack.js'; 4 | import Webhook from './webhook.js'; 5 | 6 | const NotificationServices = { 7 | Discord, 8 | Telegram, 9 | Slack, 10 | Webhook, 11 | }; 12 | 13 | export default NotificationServices; 14 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | 4 | import emailExistsMiddleware from '../middleware/auth/emailExists.js'; 5 | import { register, login, logout, setup } from '../middleware/auth/index.js'; 6 | 7 | router.post('/user/exists', emailExistsMiddleware); 8 | router.post('/register', register); 9 | router.post('/setup', setup); 10 | router.post('/login', login); 11 | router.get('/logout', logout); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import authorization from '../middleware/authorization.js'; 2 | import authRoutes from './auth.js'; 3 | import monitorRoutes from './monitor.js'; 4 | import notificationRoutes from './notifications.js'; 5 | import userRoutes from './user.js'; 6 | import statusPagesRoutes from './statusPages.js'; 7 | import statusApiRoutes from './statusApi.js'; 8 | import defaultPageMiddleware from '../middleware/status/defaultPage.js'; 9 | import setupExistsMiddleware from '../middleware/setupExists.js'; 10 | import getStatusPageUsingIdMiddleware from '../middleware/status/statusPageUsingId.js'; 11 | 12 | const initialiseRoutes = async (app) => { 13 | app.use(setupExistsMiddleware); 14 | app.use('/auth', authRoutes); 15 | app.get('/', defaultPageMiddleware); 16 | app.get('/status/:id', getStatusPageUsingIdMiddleware); 17 | // Routes used for fetching public status pages 18 | app.use('/api/status', statusApiRoutes); 19 | app.use(authorization); 20 | app.use('/api/monitor', monitorRoutes); 21 | app.use('/api/user', userRoutes); 22 | app.use('/api/notifications', notificationRoutes); 23 | // Routes used for configuring status pages 24 | app.use('/api/status-pages', statusPagesRoutes); 25 | }; 26 | 27 | export default initialiseRoutes; 28 | -------------------------------------------------------------------------------- /server/routes/monitor.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import express from 'express'; 3 | const router = express.Router(); 4 | 5 | // import local files 6 | import monitorAdd from '../middleware/monitor/add.js'; 7 | import monitorEdit from '../middleware/monitor/edit.js'; 8 | import monitorDelete from '../middleware/monitor/delete.js'; 9 | import fetchMonitorUsingId from '../middleware/monitor/id.js'; 10 | import fetchMonitorStatus from '../middleware/monitor/status.js'; 11 | import monitorPause from '../middleware/monitor/pause.js'; 12 | import { hasRequiredPermission } from '../middleware/user/hasPermission.js'; 13 | import { PermissionsBits } from '../../shared/permissions/bitFlags.js'; 14 | 15 | const canEditMonitors = hasRequiredPermission(PermissionsBits.MANAGE_MONITORS); 16 | 17 | router.get('/status', fetchMonitorStatus); 18 | router.get('/id', fetchMonitorUsingId); 19 | 20 | router.use(canEditMonitors); 21 | router.post('/add', monitorAdd); 22 | router.post('/edit', monitorEdit); 23 | router.get('/delete', monitorDelete); 24 | router.post('/pause', monitorPause); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /server/routes/notifications.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import NotificationCreateMiddleware from '../middleware/notifications/create.js'; 3 | import NotificationEditMiddleware from '../middleware/notifications/edit.js'; 4 | import NotificationGetAllMiddleware from '../middleware/notifications/getAll.js'; 5 | import NotificationGetUsingIdMiddleware from '../middleware/notifications/getUsingId.js'; 6 | import NotificationDeleteMiddleware from '../middleware/notifications/delete.js'; 7 | import NotificationToggleMiddleware from '../middleware/notifications/disable.js'; 8 | import { hasRequiredPermission } from '../middleware/user/hasPermission.js'; 9 | import { PermissionsBits } from '../../shared/permissions/bitFlags.js'; 10 | 11 | const router = Router(); 12 | 13 | const hasEditorPermissions = hasRequiredPermission( 14 | PermissionsBits.MANAGE_NOTIFICATIONS 15 | ); 16 | 17 | router.get('/', NotificationGetAllMiddleware); 18 | 19 | router.use(hasEditorPermissions); 20 | router.get('/id', NotificationGetUsingIdMiddleware); 21 | router.post('/create', NotificationCreateMiddleware); 22 | router.post('/edit', NotificationEditMiddleware); 23 | router.get('/delete', NotificationDeleteMiddleware); 24 | router.get('/toggle', NotificationToggleMiddleware); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /server/routes/statusPages.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import createStatusPageMiddleware from '../middleware/status/create.js'; 3 | import getUsingIdMiddleware from '../middleware/status/getUsingId.js'; 4 | import getAllStatusPagesMiddleware from '../middleware/status/getAll.js'; 5 | import editStatusPageMiddleware from '../middleware/status/edit.js'; 6 | import deleteStatusPageMiddleware from '../middleware/status/delete.js'; 7 | import { PermissionsBits } from '../../shared/permissions/bitFlags.js'; 8 | import { hasRequiredPermission } from '../middleware/user/hasPermission.js'; 9 | 10 | const router = express.Router(); 11 | 12 | const hasEditorPermissions = hasRequiredPermission( 13 | PermissionsBits.MANAGE_STATUS_PAGES 14 | ); 15 | 16 | router.get('/', getAllStatusPagesMiddleware); 17 | 18 | router.get('/id', getUsingIdMiddleware); 19 | 20 | router.use(hasEditorPermissions); 21 | 22 | router.post('/create', createStatusPageMiddleware); 23 | 24 | router.post('/update', editStatusPageMiddleware); 25 | 26 | router.post('/delete', deleteStatusPageMiddleware); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /server/utils/errors.js: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorizationError, 3 | ConflictError, 4 | UnprocessableError, 5 | NotificationValidatorError, 6 | } from '../../shared/utils/errors.js'; 7 | import logger from '../utils/logger.js'; 8 | 9 | const handleError = (error, response) => { 10 | logger.error('Error handler', { error: error.message, stack: error.stack }); 11 | 12 | if (!response.headersSent) { 13 | if (error instanceof AuthorizationError) { 14 | return response.status(401).send({ 15 | message: error.message, 16 | }); 17 | } 18 | 19 | if (error instanceof ConflictError) { 20 | return response.status(409).send({ 21 | message: error.message, 22 | }); 23 | } 24 | 25 | if (error instanceof UnprocessableError) { 26 | return response.status(422).send({ 27 | message: error.message, 28 | }); 29 | } 30 | 31 | if (error instanceof NotificationValidatorError) { 32 | return response.status(422).send({ 33 | [error.key]: error.message, 34 | }); 35 | } 36 | 37 | return response.status(500).send({ 38 | message: 'Something went wrong', 39 | }); 40 | } 41 | }; 42 | 43 | export { handleError }; 44 | -------------------------------------------------------------------------------- /server/utils/hashPassword.js: -------------------------------------------------------------------------------- 1 | import { hashSync, compareSync } from 'bcryptjs'; 2 | const saltRounds = 10; 3 | 4 | /** 5 | * Hash a password 6 | * @param {string} password 7 | * @returns {string} 8 | */ 9 | 10 | export const generateHash = (password) => { 11 | return hashSync(password, saltRounds); 12 | }; 13 | 14 | /** 15 | * Check if the user password matches the user hash password 16 | * @param {string} password 17 | * @param {string} hashPassword 18 | * @returns {boolean} Returns boolean if password matches 19 | */ 20 | 21 | export const verifyPassword = (password, hashPassword) => { 22 | return compareSync(password, hashPassword); 23 | }; 24 | -------------------------------------------------------------------------------- /server/utils/jwt.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import jwt from 'jsonwebtoken'; 3 | 4 | // import local files 5 | import logger from './logger.js'; 6 | import config from './config.js'; 7 | 8 | const verifyCookie = (value) => { 9 | try { 10 | const jwtSecret = config.get('jwtSecret'); 11 | let token = jwt.verify(value, jwtSecret, { 12 | algorithms: ['HS256'], 13 | }); 14 | return token; 15 | } catch (error) { 16 | logger.error('JWT Verify', { 17 | message: error.message, 18 | stack: error.stack, 19 | }); 20 | throw error; 21 | } 22 | }; 23 | 24 | const signCookie = (value) => { 25 | try { 26 | const jwtSecret = config.get('jwtSecret'); 27 | let token = jwt.sign(value, jwtSecret, { 28 | expiresIn: 2592000, 29 | }); 30 | return token; 31 | } catch (error) { 32 | logger.error('JWT Sign', { 33 | message: error.message, 34 | stack: error.stack, 35 | }); 36 | throw error; 37 | } 38 | }; 39 | 40 | export { verifyCookie, signCookie }; 41 | -------------------------------------------------------------------------------- /server/utils/randomId.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | const randomId = () => { 4 | return uuidv4(); 5 | }; 6 | 7 | export default randomId; 8 | -------------------------------------------------------------------------------- /server/utils/status.js: -------------------------------------------------------------------------------- 1 | const layoutCheck = (layout) => { 2 | return ( 3 | (layout.type === 'metrics' && layout.autoAdd) || 4 | (layout.type === 'uptime' && layout.graphType !== 'Basic' && layout.autoAdd) 5 | ); 6 | }; 7 | 8 | export const hasAutoAdd = (content) => { 9 | if (Array.isArray(content)) { 10 | return content.some((statusPage) => { 11 | return statusPage.layout.some(layoutCheck); 12 | }); 13 | } 14 | 15 | return content.layout.some(layoutCheck); 16 | }; 17 | 18 | export const getMonitorIds = (content) => { 19 | const monitorIds = []; 20 | 21 | if (Array.isArray(content)) { 22 | for (const statusPage of content) { 23 | statusPage.layout.forEach((item) => { 24 | if (item.monitors) { 25 | item.monitors.forEach((value) => { 26 | monitorIds.push(value?.id || value); 27 | }); 28 | } 29 | }); 30 | } 31 | } else { 32 | content.layout.forEach((item) => { 33 | if (item.monitors) { 34 | item.monitors.forEach((value) => { 35 | monitorIds.push(value?.id || value); 36 | }); 37 | } 38 | }); 39 | } 40 | 41 | return monitorIds; 42 | }; 43 | -------------------------------------------------------------------------------- /server/utils/uaParser.js: -------------------------------------------------------------------------------- 1 | import { UAParser } from 'ua-parser-js'; 2 | 3 | export const parseUserAgent = (userAgent) => { 4 | const { browser, device, os } = UAParser(userAgent); 5 | 6 | if (device.type === 'mobile') { 7 | return { 8 | data: { browser, device, os }, 9 | device: { 10 | os: os.name || 'Unknown', 11 | browser: browser.name || 'Unknown', 12 | type: 'mobile', 13 | }, 14 | }; 15 | } else if (device.type === 'tablet') { 16 | return { 17 | data: { browser, device, os }, 18 | device: { 19 | os: os.name || 'Unknown', 20 | browser: browser.name || 'Unknown', 21 | type: 'tablet', 22 | }, 23 | }; 24 | } 25 | 26 | return { 27 | data: { browser, device, os }, 28 | device: { 29 | os: os.name || 'Unknown', 30 | browser: browser.name || 'Unknown', 31 | type: 'desktop', 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /shared/notifications/index.js: -------------------------------------------------------------------------------- 1 | import { DiscordTemplateMessages } from './discord'; 2 | import { SlackTemplateMessages } from './slack'; 3 | import { TelegramTemplateMessages } from './telegram'; 4 | import { WebhookTemplateMessages } from './webhook'; 5 | 6 | const NotificationsTemplates = { 7 | Discord: DiscordTemplateMessages, 8 | Slack: SlackTemplateMessages, 9 | Telegram: TelegramTemplateMessages, 10 | Webhook: WebhookTemplateMessages, 11 | }; 12 | 13 | export default NotificationsTemplates; 14 | -------------------------------------------------------------------------------- /shared/notifications/replacers/date.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | const dateFormatRegex = /\{\{\s*date\s*\[(.+?)\]\s*\}\}/g; 4 | 5 | const DateReplacer = (text, heartbeat = {}) => { 6 | return text.replace(dateFormatRegex, (_, format) => { 7 | const date = dayjs(heartbeat.date).format(format); 8 | return date; 9 | }); 10 | }; 11 | 12 | const hasDate = (text) => dateFormatRegex.test(text); 13 | 14 | export { hasDate, DateReplacer }; 15 | -------------------------------------------------------------------------------- /shared/notifications/telegram.js: -------------------------------------------------------------------------------- 1 | const TelegramTemplateMessages = { 2 | basic: '*Triggered: Service {{service_name}} is currently down!*', 3 | pretty: 4 | '*Triggered: Service {{service_name}} is currently down!*\n\n*Service Name*\n{{service_name}}\n\n*Service Address*\n{{service_address}}\n\n*Latency*\n{{heartbeat_latency}} ms\n\n*Error*\n{{heartbeat_message}}', 5 | nerdy: 6 | '*Triggered: Service {{service_name}} is currently down!*\n\n*Service*\n```{{service_json}}```\n\n*Heartbeat*\n```{{heartbeat_json}}```', 7 | recovery: '*Service {{service_name}} is back up!*', 8 | }; 9 | 10 | export { TelegramTemplateMessages }; 11 | -------------------------------------------------------------------------------- /shared/parseJson.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export const loadJSON = (file) => { 5 | const fullPath = path.join(process.cwd(), file); 6 | 7 | if (!fs.existsSync(fullPath)) { 8 | return {}; 9 | } 10 | 11 | return JSON.parse(fs.readFileSync(fullPath, 'utf8')); 12 | }; 13 | -------------------------------------------------------------------------------- /shared/permissions/bitFlags.js: -------------------------------------------------------------------------------- 1 | export const PermissionsBits = { 2 | ADMINISTRATOR: 1 << 0, 3 | VIEW_MONITORS: 1 << 1, 4 | MANAGE_MONITORS: 1 << 2, 5 | VIEW_NOTIFICATIONS: 1 << 3, 6 | MANAGE_NOTIFICATIONS: 1 << 4, 7 | VIEW_STATUS_PAGES: 1 << 5, 8 | MANAGE_STATUS_PAGES: 1 << 6, 9 | VIEW_INCIDENTS: 1 << 7, 10 | MANAGE_INCIDENTS: 1 << 8, 11 | MANAGE_TEAM: 1 << 9, 12 | }; 13 | -------------------------------------------------------------------------------- /shared/permissions/oldPermsToFlags.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBits } from './bitFlags.js'; 2 | 3 | export const oldPermsToFlags = { 4 | 1: PermissionsBits.ADMINISTRATOR, 5 | 2: PermissionsBits.ADMINISTRATOR, 6 | 3: 7 | PermissionsBits.VIEW_MONITORS | 8 | PermissionsBits.MANAGE_MONITORS | 9 | PermissionsBits.VIEW_NOTIFICATIONS | 10 | PermissionsBits.MANAGE_NOTIFICATIONS, 11 | 4: PermissionsBits.VIEW_MONITORS | PermissionsBits.VIEW_WEBHOOKS, 12 | }; 13 | -------------------------------------------------------------------------------- /shared/permissions/role.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBits } from './bitFlags.js'; 2 | 3 | class Role { 4 | constructor(name, permissionFlags = 0) { 5 | this.name = name; 6 | this.permissionFlags = permissionFlags; 7 | } 8 | 9 | hasPermission(permission) { 10 | if (this.permissionFlags === PermissionsBits.ADMINISTRATOR) { 11 | return true; 12 | } 13 | 14 | return (this.permissionFlags & permission) === permission; 15 | } 16 | 17 | addPermission(permission) { 18 | this.permissionFlags |= permission; 19 | } 20 | 21 | removePermission(permission) { 22 | this.permissionFlags &= ~permission; 23 | } 24 | } 25 | 26 | export default Role; 27 | -------------------------------------------------------------------------------- /shared/permissions/user.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBits } from './bitFlags'; 2 | 3 | class User { 4 | constructor(user, roles = []) { 5 | this.user = user; 6 | this.roles = roles; 7 | this._permissions = this.calculateEffectivePermissions(); 8 | } 9 | 10 | calculateEffectivePermissions() { 11 | return this.roles.reduce((acc, role) => acc | role.permissionFlags, 0); 12 | } 13 | 14 | get permissions() { 15 | return this._permissions; 16 | } 17 | 18 | setRoles(newRoles) { 19 | this.roles = newRoles; 20 | this._permissions = this.calculateEffectivePermissions(); 21 | } 22 | 23 | addRole(role) { 24 | this.roles.push(role); 25 | this._permissions |= role.permissionFlags; 26 | } 27 | 28 | hasPermission(permission) { 29 | if (this._permissions === PermissionsBits.ADMINISTRATOR) { 30 | return true; 31 | } 32 | 33 | return (this._permissions & permission) === permission; 34 | } 35 | } 36 | 37 | export default User; 38 | -------------------------------------------------------------------------------- /shared/permissions/validate.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBits } from './bitFlags.js'; 2 | 3 | const isPowerOfTwo = (n) => { 4 | return n > 0 && (n & (n - 1)) === 0; 5 | }; 6 | 7 | export const isValidPermission = (permission) => { 8 | return ( 9 | isPowerOfTwo(permission) && 10 | Object.hasOwnProperty.call(PermissionsBits, permission) 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /shared/schema/status/customCSS.js: -------------------------------------------------------------------------------- 1 | const StatusCustomCSSSchema = { 2 | id: { _type: 'string', _required: true }, 3 | type: { 4 | _type: 'string', 5 | _validate: (value) => value === 'customCSS', 6 | _required: true, 7 | }, 8 | content: { 9 | _type: 'string', 10 | _validate: (value) => value?.length > 0, 11 | _required: true, 12 | }, 13 | }; 14 | 15 | export default StatusCustomCSSSchema; 16 | -------------------------------------------------------------------------------- /shared/schema/status/customHTML.js: -------------------------------------------------------------------------------- 1 | const StatusCustomHTMLSchema = { 2 | id: { _type: 'string', _required: true }, 3 | type: { 4 | _type: 'string', 5 | _validate: (value) => value === 'customHTML', 6 | _required: true, 7 | }, 8 | content: { 9 | _type: 'string', 10 | _validate: (value) => value?.length > 0, 11 | _required: true, 12 | }, 13 | }; 14 | 15 | export default StatusCustomHTMLSchema; 16 | -------------------------------------------------------------------------------- /shared/schema/status/history.js: -------------------------------------------------------------------------------- 1 | const StatusCustomHTMLSchema = { 2 | id: { _type: 'string', _required: true }, 3 | type: { 4 | _type: 'string', 5 | _validate: (value) => value === 'history', 6 | _required: true, 7 | }, 8 | }; 9 | 10 | export default StatusCustomHTMLSchema; 11 | -------------------------------------------------------------------------------- /shared/schema/status/incidents.js: -------------------------------------------------------------------------------- 1 | import { 2 | statusDesign, 3 | statusIncidents, 4 | statusSizes, 5 | } from '../../constants/status.js'; 6 | 7 | const StatusIncidentsSchema = { 8 | id: { _type: 'string', _required: true }, 9 | type: { 10 | _type: 'string', 11 | _validate: (value) => value === 'incidents', 12 | _required: true, 13 | }, 14 | design: { 15 | _type: 'string', 16 | _validate: (value) => statusDesign.includes(value), 17 | _required: true, 18 | }, 19 | size: { 20 | _type: 'string', 21 | _validate: (value) => statusSizes.includes(value), 22 | _required: true, 23 | }, 24 | titleSize: { 25 | _type: 'string', 26 | _validate: (value) => statusSizes.includes(value), 27 | _required: true, 28 | }, 29 | status: { 30 | _type: 'string', 31 | _validate: (value) => statusIncidents.includes(value), 32 | _required: true, 33 | }, 34 | }; 35 | 36 | export default StatusIncidentsSchema; 37 | -------------------------------------------------------------------------------- /shared/schema/status/index.js: -------------------------------------------------------------------------------- 1 | export { default as StatusCustomCSSSchema } from './customCSS.js'; 2 | export { default as StatusCustomHTMLSchema } from './customHTML.js'; 3 | export { default as StatusHeaderSchema } from './header.js'; 4 | export { default as StatusHistorySchema } from './history.js'; 5 | export { default as StatusIncidentsSchema } from './incidents.js'; 6 | export { default as StatusMetricsSchema } from './metrics.js'; 7 | export { default as StatusStatusSchema } from './status.js'; 8 | export { default as StatusUptimeSchema } from './uptime.js'; 9 | -------------------------------------------------------------------------------- /shared/schema/status/status.js: -------------------------------------------------------------------------------- 1 | import { 2 | statusBarDesign, 3 | statusIncidents, 4 | statusSizes, 5 | } from '../../constants/status.js'; 6 | 7 | const StatusSchema = { 8 | id: { _type: 'string', _required: true }, 9 | type: { 10 | _type: 'string', 11 | _validate: (value) => value === 'status', 12 | _required: true, 13 | }, 14 | icon: { _type: 'boolean', _required: true }, 15 | design: { 16 | _type: 'string', 17 | _validate: (value) => statusBarDesign.includes(value), 18 | _required: true, 19 | }, 20 | size: { 21 | _type: 'string', 22 | _validate: (value) => statusSizes.includes(value), 23 | _required: true, 24 | }, 25 | titleSize: { 26 | _type: 'string', 27 | _validate: (value) => statusSizes.includes(value), 28 | _required: true, 29 | }, 30 | status: { 31 | _type: 'string', 32 | _validate: (value) => statusIncidents.includes(value), 33 | _required: true, 34 | }, 35 | }; 36 | 37 | export default StatusSchema; 38 | -------------------------------------------------------------------------------- /shared/schema/status/uptime.js: -------------------------------------------------------------------------------- 1 | import { 2 | statusGraphDesigns, 3 | statusIncidents, 4 | statusIndicators, 5 | } from '../../constants/status.js'; 6 | 7 | const titleRegex = /^[a-zA-Z0-9_ -]{0,64}$/; 8 | 9 | const StatusUptimeSchema = { 10 | id: { _type: 'string', _required: true }, 11 | type: { 12 | _type: 'string', 13 | _validate: (value) => value === 'uptime', 14 | _required: true, 15 | }, 16 | title: { 17 | _type: 'string', 18 | _validate: (value) => titleRegex.test(value), 19 | _required: true, 20 | }, 21 | monitors: { _type: 'array', _required: true }, 22 | autoAdd: { _type: 'boolean', _required: true }, 23 | graphType: { 24 | _type: 'string', 25 | _validate: (value) => statusGraphDesigns.includes(value), 26 | _required: true, 27 | }, 28 | statusIndicator: { 29 | _type: 'string', 30 | _validate: (value) => statusIndicators.includes(value), 31 | _required: true, 32 | }, 33 | status: { 34 | _type: 'string', 35 | _validate: (value) => statusIncidents.includes(value), 36 | _required: true, 37 | }, 38 | }; 39 | 40 | export default StatusUptimeSchema; 41 | -------------------------------------------------------------------------------- /shared/utils/object.js: -------------------------------------------------------------------------------- 1 | export const isEmpty = (object) => { 2 | for (let prop in object) { 3 | if (Object.prototype.hasOwnProperty.call(object, prop)) return false; 4 | } 5 | 6 | return true; 7 | }; 8 | -------------------------------------------------------------------------------- /shared/utils/url.js: -------------------------------------------------------------------------------- 1 | const createURL = (path = '', params) => { 2 | if (!path || !params) return path; 3 | 4 | if (path.includes('?')) { 5 | const [pathWithoutQuery, queries] = path.split('?'); 6 | const queriesAsObject = {}; 7 | 8 | queries.split('&').forEach((query) => { 9 | const [key, value] = query.split('='); 10 | queriesAsObject[key] = value; 11 | }); 12 | 13 | const searchParams = new URLSearchParams({ 14 | ...queriesAsObject, 15 | ...params, 16 | }).toString(); 17 | 18 | return `${pathWithoutQuery}?${searchParams}`; 19 | } 20 | 21 | const searchParams = new URLSearchParams(params).toString(); 22 | 23 | return `${path}?${searchParams}`; 24 | }; 25 | 26 | export { createURL }; 27 | -------------------------------------------------------------------------------- /shared/validators/index.js: -------------------------------------------------------------------------------- 1 | import auth from './auth.js'; 2 | import * as monitor from './monitor.js'; 3 | import * as user from './user.js'; 4 | 5 | const validators = { auth, monitor, user }; 6 | 7 | export default validators; 8 | -------------------------------------------------------------------------------- /shared/validators/notifications/index.js: -------------------------------------------------------------------------------- 1 | import Discord from './discord.js'; 2 | import Slack from './slack.js'; 3 | import Telegram from './telegram.js'; 4 | import Webhook from './webhook.js'; 5 | 6 | const NotificationValidators = { Discord, Telegram, Slack, Webhook }; 7 | 8 | export default NotificationValidators; 9 | -------------------------------------------------------------------------------- /shared/validators/user.js: -------------------------------------------------------------------------------- 1 | const defaultAvatars = [ 2 | 'Ape', 3 | 'Bear', 4 | 'Cat', 5 | 'Dog', 6 | 'Doggo', 7 | 'Duck', 8 | 'Eagle', 9 | 'Fox', 10 | 'Gerbil', 11 | 'Hamster', 12 | 'Hedgehog', 13 | 'Koala', 14 | 'Ostrich', 15 | 'Panda', 16 | 'Rabbit', 17 | 'Rocket', 18 | 'Tiger', 19 | ]; 20 | 21 | const isImageUrl = (url) => { 22 | if (typeof url !== 'string') { 23 | return false; 24 | } 25 | 26 | return url.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/gim); 27 | }; 28 | 29 | const isAvatar = (avatar) => { 30 | if (avatar === null) { 31 | return false; 32 | } 33 | 34 | if (!defaultAvatars.includes(avatar) && !isImageUrl(avatar)) { 35 | return 'Avatar must be a valid image URL or one of the default avatars.'; 36 | } 37 | 38 | return false; 39 | }; 40 | 41 | export { isAvatar }; 42 | -------------------------------------------------------------------------------- /test/e2e/setup/fixtures/notifications/discord.json: -------------------------------------------------------------------------------- 1 | { 2 | "friendlyName": { 3 | "id": "[id=\"friendly-name\"]", 4 | "value": "Discord", 5 | "invalidValue": "{}[]||<>", 6 | "type": "text", 7 | "error": { 8 | "id": "[id=\"text-input-error-friendly-name\"]", 9 | "value": "Invalid Friendly Name. Must be alphanumeric, dashes, and underscores only." 10 | } 11 | }, 12 | "url": { 13 | "id": "[id=\"webhook-url\"]", 14 | "value": "https://discord.com/api/webhooks/082399/lunalytics", 15 | "invalidValue": "https://discord.com/api/webhook/082399/lunalytics", 16 | "type": "text", 17 | "error": { 18 | "id": "[id=\"text-input-error-webhook-url\"]", 19 | "value": "Invalid Discord Webhook URL" 20 | } 21 | }, 22 | "username": { 23 | "id": "[id=\"webhook-username\"]", 24 | "value": "Lunalytics", 25 | "invalidValue": "{}[]||<>", 26 | "type": "text", 27 | "error": { 28 | "id": "[id=\"text-input-error-webhook-username\"]", 29 | "value": "Invalid Discord Webhook Username" 30 | } 31 | }, 32 | "message": { 33 | "id": "[id=\"text-messsage\"]", 34 | "value": "@everyone Alert!", 35 | "type": "text" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/e2e/setup/fixtures/notifications/telegram.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": { 3 | "id": "[id=\"notification-type-dropdown\"]", 4 | "value": "[id=\"notification-type-Telegram\"]", 5 | "type": "dropdown" 6 | }, 7 | "friendlyName": { 8 | "id": "[id=\"friendly-name\"]", 9 | "value": "Slack", 10 | "invalidValue": "{}[]||<>", 11 | "type": "text", 12 | "error": { 13 | "id": "[id=\"text-input-error-friendly-name\"]", 14 | "value": "Invalid Friendly Name. Must be alphanumeric, dashes, and underscores only." 15 | } 16 | }, 17 | "url": { 18 | "id": "[id=\"bot-token\"]", 19 | "value": "123123123123123123", 20 | "invalidValue": "{}[]||<>", 21 | "type": "text", 22 | "error": { 23 | "id": "[id=\"text-input-error-bot-token\"]", 24 | "value": "Invalid Telegram Bot Token" 25 | } 26 | }, 27 | "chatId": { 28 | "id": "[id=\"chat-id\"]", 29 | "value": "123123123", 30 | "invalidValue": "{}[]||<>", 31 | "type": "text", 32 | "error": { 33 | "id": "[id=\"text-input-error-chat-id\"]", 34 | "value": "Invalid Chat ID" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/e2e/setup/fixtures/notifications/webhooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": { 3 | "id": "[id=\"notification-type-dropdown\"]", 4 | "value": "[id=\"notification-type-Webhook\"]", 5 | "type": "dropdown" 6 | }, 7 | "friendlyName": { 8 | "id": "[id=\"friendly-name\"]", 9 | "value": "Webhook", 10 | "invalidValue": "{}[]||<>", 11 | "type": "text", 12 | "error": { 13 | "id": "[id=\"text-input-error-friendly-name\"]", 14 | "value": "Invalid Friendly Name. Must be alphanumeric, dashes, and underscores only." 15 | } 16 | }, 17 | "url": { 18 | "id": "[id=\"webhook-url\"]", 19 | "value": "https://lunalytics.xyz/webhooks/example", 20 | "invalidValue": "this is not a webhook url", 21 | "type": "text", 22 | "error": { 23 | "id": "[id=\"text-input-error-webhook-url\"]", 24 | "value": "Invalid Webhook URL" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/setup/support/e2e.js: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | -------------------------------------------------------------------------------- /test/server/classes/certificate.test.js: -------------------------------------------------------------------------------- 1 | import cleanCertificate from '../../../server/class/certificate'; 2 | 3 | describe('Certificate - Class', () => { 4 | it('should a valid clean certificate', () => { 5 | const certificate = { 6 | isValid: true, 7 | issuer: JSON.stringify({ C: 'US', O: "Let's Encrypt", CN: 'R3' }), 8 | validFrom: 'Apr 15 01:56:22 2024 GMT', 9 | validTill: 'Jul 14 01:56:21 2024 GMT', 10 | validOn: JSON.stringify(['*.vercel.app', 'vercel.app']), 11 | daysRemaining: 62, 12 | nextCheck: 1715646231877, 13 | }; 14 | 15 | expect(cleanCertificate(certificate)).toEqual({ 16 | isValid: true, 17 | issuer: { C: 'US', O: "Let's Encrypt", CN: 'R3' }, 18 | validFrom: 'Apr 15 01:56:22 2024 GMT', 19 | validTill: 'Jul 14 01:56:21 2024 GMT', 20 | validOn: ['*.vercel.app', 'vercel.app'], 21 | daysRemaining: 62, 22 | nextCheck: 1715646231877, 23 | }); 24 | }); 25 | 26 | it('should a invalid clean certificate', () => { 27 | const certificate = { isValid: false }; 28 | 29 | expect(cleanCertificate(certificate)).toEqual({ 30 | isValid: false, 31 | issuer: '', 32 | validOn: '', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/server/middleware/auth/logout.test.js: -------------------------------------------------------------------------------- 1 | import { createRequest, createResponse } from 'node-mocks-http'; 2 | import { deleteCookie } from '../../../../shared/utils/cookies'; 3 | import logout from '../../../../server/middleware/auth/logout'; 4 | import { createURL } from '../../../../shared/utils/url'; 5 | 6 | vi.mock('../../../../shared/utils/cookies'); 7 | 8 | describe('Logout - Middleware', () => { 9 | let fakeRequest; 10 | let fakeResponse; 11 | 12 | beforeEach(() => { 13 | fakeRequest = createRequest(); 14 | fakeResponse = createResponse(); 15 | 16 | deleteCookie = vi.fn(); 17 | fakeResponse.redirect = vi.fn(); 18 | }); 19 | 20 | afterEach(() => { 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | it('should call deleteCookie with response and "session_token"', async () => { 25 | await logout(fakeRequest, fakeResponse); 26 | 27 | expect(deleteCookie).toHaveBeenCalledWith(fakeResponse, 'session_token'); 28 | }); 29 | 30 | it('should redirect to /login', async () => { 31 | await logout(fakeRequest, fakeResponse); 32 | 33 | expect(fakeResponse.redirect).toHaveBeenCalledWith(createURL('/login')); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { visualizer } from 'rollup-plugin-visualizer'; 4 | import { compression } from 'vite-plugin-compression2'; 5 | 6 | const filter = /\.(js|mjs|json|css|svg|html)$/i; 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | react(), 11 | compression({ algorithm: 'gzip', filter }), 12 | compression({ algorithm: 'brotliCompress', filter }), 13 | visualizer({ filename: './stats/stats.html' }), 14 | ], 15 | define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version) }, 16 | css: { preprocessorOptions: { scss: { api: 'modern-compiler' } } }, 17 | }); 18 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | watch: false, 7 | include: ['test/server/**/*.test.{js,jsx,ts,tsx}'], 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------