Compare commits

...

376 Commits

Author SHA1 Message Date
Nawaz Dhandala
4868e285b0 fix: simplify description for postmortem published timestamp field 2025-12-03 19:30:08 +00:00
Nawaz Dhandala
f572eb6f93 feat: add subscriber notification fields and migration for postmortem 2025-12-03 19:18:43 +00:00
Nawaz Dhandala
a0868e2f75 feat: add subscriber notification status and resend functionality in postmortem 2025-12-03 19:16:49 +00:00
Nawaz Dhandala
3dfd7a9206 feat: set subscriber notification status to Pending on postmortem published 2025-12-03 19:12:42 +00:00
Nawaz Dhandala
d7582337bf fix: remove ignoreHooks option from incident status update in postmortem notification 2025-12-03 19:11:00 +00:00
Nawaz Dhandala
23043462d7 fix: correct incident number retrieval in postmortem notification logic 2025-12-03 19:08:29 +00:00
Nawaz Dhandala
76d53c53c8 feat: add notifySubscribersOnPostmortemPublished toggle to postmortem form 2025-12-03 19:06:40 +00:00
Nawaz Dhandala
4437e912a3 feat: add notifySubscribersOnPostmortemPublished field and update notification logic 2025-12-03 19:05:28 +00:00
Nawaz Dhandala
937d4675a8 feat: add postmortem notification system for subscribers 2025-12-03 19:03:02 +00:00
Nawaz Dhandala
3cc984f149 Merge branch 'master' into release 2025-12-03 18:53:12 +00:00
Nawaz Dhandala
991928a5a5 fix: update version to 9.2.1 2025-12-03 18:53:05 +00:00
Nawaz Dhandala
6f46812418 fix: remove welcome message sending on bot installation 2025-12-03 18:52:18 +00:00
Nawaz Dhandala
6e20e7f08f fix: correct typo in debug log message for sleep duration in FetchListAndProbe 2025-12-03 18:45:25 +00:00
Nawaz Dhandala
ae406d8ee1 fix: update QEMU setup to use tonistiigi/binfmt:qemu-v10.0.4 in release workflows 2025-12-03 18:42:01 +00:00
Nawaz Dhandala
05920d5b99 fix: improve error logging for Playwright resource closure 2025-12-03 18:33:27 +00:00
Nawaz Dhandala
3a309aabcf fix: enhance browser context closure handling in SyntheticMonitor 2025-12-03 18:31:12 +00:00
Nawaz Dhandala
22a3004a3f fix: simplify error logging in safeCloseBrowserContext and adjust formatting in safeCloseBrowser 2025-12-03 18:27:09 +00:00
Nawaz Dhandala
b8f69fbea3 fix: refactor browser session management in SyntheticMonitor for improved clarity and error handling 2025-12-03 18:25:37 +00:00
Nawaz Dhandala
888aff6392 fix: format migration queries and update index for new migration 2025-12-03 14:42:10 +00:00
Nawaz Dhandala
234de977c4 fix: prevent rendering icon for non-highlighted timeline items 2025-12-03 14:41:39 +00:00
Nawaz Dhandala
fa5f606709 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-12-03 14:20:38 +00:00
Nawaz Dhandala
b889611d16 fix: update postmortem attachment message for improved visibility and change version to 9.2.0 2025-12-03 14:04:09 +00:00
Nawaz Dhandala
43f0eeb0f8 fix: improve clarity in postmortem status page description and enhance attachment handling messages 2025-12-03 13:57:40 +00:00
Nawaz Dhandala
be311dd8b5 feat: add default value for Postmortem Published At field 2025-12-03 13:56:30 +00:00
Nawaz Dhandala
29428bf660 fix: update placeholder for Postmortem Published At field to improve clarity 2025-12-03 13:55:58 +00:00
Simon Larsen
9eebbe9dfb feat: implement retry logic with configurable attempts and delay for LMStudioClient 2025-12-03 13:51:30 +00:00
Nawaz Dhandala
0dc3bb4f33 feat: add MigrationName1764767371788 for updating OnCallDutyPolicyScheduleLayer defaults 2025-12-03 13:10:12 +00:00
Nawaz Dhandala
adf5a9c1f3 feat: add postmortemPostedAt field and update related components for incident tracking 2025-12-03 13:09:23 +00:00
Nawaz Dhandala
faaded049a feat: add DocumentCheck icon to IconProp and update incident postmortem note icon 2025-12-03 12:58:27 +00:00
Nawaz Dhandala
d02e3882be fix: adjust font size classes for TimelineItem highlight to improve readability 2025-12-03 12:52:20 +00:00
Nawaz Dhandala
e1af84fafa feat: enhance EventItem styling with conditional highlight for improved visibility 2025-12-03 12:51:03 +00:00
Nawaz Dhandala
c371f0a25f feat: add title and highlight properties to TimelineItem for enhanced event display 2025-12-03 12:42:17 +00:00
Nawaz Dhandala
c86d2c2a4a refactor: streamline API route registration and improve code readability 2025-12-03 11:50:17 +00:00
Nawaz Dhandala
a807cc10ab feat: add migration for IncidentPostmortemAttachmentFile table and related constraints 2025-12-03 11:43:31 +00:00
Nawaz Dhandala
6cc480744d Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-12-03 11:42:07 +00:00
Nawaz Dhandala
7cb6104795 feat: enhance postmortem functionality with attachment handling and status page visibility 2025-12-03 11:42:05 +00:00
Nawaz Dhandala
49dd315501 feat: implement postmortem attachment handling in Incident and StatusPage APIs 2025-12-03 11:34:00 +00:00
Simon Larsen
9ec2b458ed style: remove unnecessary first-letter styling from blog post body 2025-12-03 11:14:50 +00:00
Nawaz Dhandala
702b5811a9 fix: add missing line continuation for workspace-path in usage example 2025-12-02 22:25:59 +00:00
Nawaz Dhandala
7dc7255790 feat: add alias normalization for historical argument names in ReadFileTool 2025-12-02 22:22:19 +00:00
Nawaz Dhandala
1f620e7092 feat: update default max iterations for tool-calling rounds to 100 2025-12-02 21:48:32 +00:00
Nawaz Dhandala
87466246fa Revert "feat: enhance log file path resolution in CLI to support home directory and relative paths"
This reverts commit 12eaa17859.
2025-12-02 21:34:16 +00:00
Nawaz Dhandala
12eaa17859 feat: enhance log file path resolution in CLI to support home directory and relative paths 2025-12-02 21:31:20 +00:00
Nawaz Dhandala
e782ae6b3c feat: enhance logging in CopilotAgent and WorkspaceContext with detailed message contents 2025-12-02 21:17:34 +00:00
Nawaz Dhandala
9ad87328c2 feat: add detailed JSDoc comments for Copilot agent and tools 2025-12-02 21:04:08 +00:00
Nawaz Dhandala
8279294d15 feat: implement oneuptime-copilot-agent CLI with logging and configuration options 2025-12-02 20:52:15 +00:00
Nawaz Dhandala
8c6da51d58 Adopt PascalCase paths in Copilot 2025-12-02 20:34:27 +00:00
Nawaz Dhandala
6d114e3ac4 chore: bump version to 9.1.3 2025-12-02 20:18:07 +00:00
Nawaz Dhandala
44427d3ee7 feat: enhance ReadFileTool with optional line start and end parameters 2025-12-02 14:52:07 +00:00
Nawaz Dhandala
09b0c3b1ef feat: add debug logging instructions and example to README 2025-12-02 14:42:08 +00:00
Nawaz Dhandala
ad597fe5dd feat: update model name and workspace path in usage examples 2025-12-02 14:39:36 +00:00
Nawaz Dhandala
74f17fa45c fix: handle notification skipping for already notified scheduled maintenance events 2025-12-02 14:22:10 +00:00
Nawaz Dhandala
b19a5fa58a feat: add isCreatedState and isScheduledState checks to skip notifications for already notified incidents 2025-12-02 14:21:52 +00:00
Nawaz Dhandala
57abffa113 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-12-02 13:06:53 +00:00
Nawaz Dhandala
e8e493ee5a Refactor code structure for improved readability and maintainability 2025-12-02 13:06:50 +00:00
Simon Larsen
e065ebdddc Merge branch 'copilot-v2' 2025-12-02 13:02:32 +00:00
Simon Larsen
39da442892 style: update blog post first paragraph styling for improved readability 2025-12-02 13:02:18 +00:00
Simon Larsen
45b02b30e3 Merge pull request #2152 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-12-02 11:46:04 +00:00
Nawaz Dhandala
30414327f9 feat: add Dockerfile for OneUptime-copilot setup 2025-12-02 11:05:16 +00:00
simlarsen
b99a20a588 chore: npm audit fix 2025-12-02 01:50:48 +00:00
Nawaz Dhandala
22178c282d fix: format command descriptions for consistency in MicrosoftTeamsAPI 2025-12-01 17:13:11 +00:00
Nawaz Dhandala
30389a8d49 feat: add command lists for improved interaction with OneUptime bot in Microsoft Teams 2025-12-01 17:11:37 +00:00
Nawaz Dhandala
7b73cc2ea7 fix: remove trailing spaces in action type definitions 2025-12-01 17:05:30 +00:00
Nawaz Dhandala
6d2c331216 feat: update command triggers for incident and maintenance actions 2025-12-01 17:05:05 +00:00
Nawaz Dhandala
624e4c2296 chore: update version to 9.1.2 2025-12-01 16:38:51 +00:00
Simon Larsen
5e901ee973 Merge pull request #2151 from OneUptime/copilot-v2
Copilot v2
2025-12-01 16:25:22 +00:00
Simon Larsen
a103abc7a9 fix: simplify boolean expression for hasProgressedBeyondScheduledState 2025-12-01 15:45:35 +00:00
Simon Larsen
a7dda0bd53 feat: add logic to update nextSubscriberNotificationBeforeTheEventAt for progressed scheduled maintenance events 2025-12-01 15:45:15 +00:00
Simon Larsen
6948754c86 Merge pull request #2147 from OneUptime/copilot-v2
Copilot v2
2025-12-01 15:21:05 +00:00
Simon Larsen
cc5731bb6d feat: add error handling and logging for missing tool calls and directory entries 2025-12-01 15:20:44 +00:00
Simon Larsen
6761a8a686 Merge pull request #2148 from OneUptime/snyk-upgrade-240d43adaab510cce84165a4f1ccf9b5
[Snyk] Upgrade mailparser from 3.7.5 to 3.9.0
2025-12-01 13:42:14 +00:00
Simon Larsen
6e487199aa refactor: add type annotations and improve type safety across multiple files 2025-12-01 13:41:34 +00:00
snyk-bot
cda5de92ec fix: upgrade mailparser from 3.7.5 to 3.9.0
Snyk has created this PR to upgrade mailparser from 3.7.5 to 3.9.0.

See this package in npm:
mailparser

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/c3622982-05c8-495c-809c-20f301c75f92?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-29 12:10:48 +00:00
Simon Larsen
33349341a9 refactor: improve code formatting and readability across multiple files 2025-11-28 21:53:50 +00:00
Simon Larsen
db81fdd3e7 feat: enhance logging throughout the Copilot agent and tools for better traceability 2025-11-28 21:52:33 +00:00
Simon Larsen
d71eba91dd chore: remove vscode-copilot-chat subproject reference 2025-11-28 21:45:07 +00:00
Simon Larsen
682bb805f3 feat: implement AgentLogger for file-based logging with exit handlers 2025-11-28 21:43:15 +00:00
Simon Larsen
7f38e3d417 docs: add usage example for running the agent in development mode 2025-11-28 21:31:08 +00:00
Simon Larsen
559985e93b feat: add tsconfig-paths for improved module resolution in development 2025-11-28 21:28:19 +00:00
Simon Larsen
43588cbe5a refactor: update optional properties to include 'undefined' type in various interfaces 2025-11-28 20:57:17 +00:00
Simon Larsen
0772fce477 refactor: update Telemetry class to use type assertions for loggerProviderConfig and nodeSdkConfiguration
chore: remove unused common type definitions and clean up tsconfig.json
2025-11-28 20:20:09 +00:00
Simon Larsen
78107d8b1c chore: remove unused type definitions and clean up tsconfig.json 2025-11-28 20:06:43 +00:00
Simon Larsen
078af43b0c chore: remove tsconfig.json for OneUptime Copilot Agent 2025-11-28 19:58:11 +00:00
Simon Larsen
9b9aeb2f40 feat: Implement OneUptime Copilot Agent with workspace tools
- Added SystemPrompt for guiding the agent's behavior.
- Created WorkspaceContextBuilder to gather workspace information.
- Developed main entry point in index.ts for agent execution.
- Implemented LMStudioClient for interacting with the LM Studio API.
- Added ApplyPatchTool for applying code changes via patches.
- Created ListDirectoryTool for listing files and directories.
- Implemented ReadFileTool for reading file contents.
- Developed RunCommandTool for executing shell commands.
- Added SearchWorkspaceTool for searching files in the workspace.
- Created WriteFileTool for writing content to files.
- Established ToolRegistry for managing and executing tools.
- Defined types for chat messages and tool calls.
- Added utility classes for logging and executing commands.
- Implemented WorkspacePaths for managing file paths within the workspace.
- Configured TypeScript settings in tsconfig.json.
2025-11-28 19:57:52 +00:00
Nawaz Dhandala
67577f5a2b refactor: improve formatting and readability in Incident migration and MonitorService 2025-11-28 17:42:22 +00:00
Nawaz Dhandala
4e808cf382 feat: enhance monitor deletion process to include MetricService cleanup 2025-11-28 17:40:31 +00:00
Nawaz Dhandala
c993b33dab feat: add projectId to MetricService deletion query in incident handling 2025-11-28 17:35:23 +00:00
Nawaz Dhandala
3c5a64024b feat: include projectId in MetricService deletion query for incidents 2025-11-28 17:34:30 +00:00
Nawaz Dhandala
86efe54a29 refactor: remove unused favicon handling from DashboardMasterPage 2025-11-28 17:29:43 +00:00
Simon Larsen
17bf568428 feat: Implement OneUptime Copilot Agent with core functionalities
- Add SystemPrompt to define agent behavior and principles.
- Create WorkspaceContextBuilder for workspace snapshot and Git status.
- Initialize main entry point with command-line options for agent configuration.
- Develop LMStudioClient for chat completion requests to LM Studio.
- Implement tools for file operations: ApplyPatchTool, ListDirectoryTool, ReadFileTool, RunCommandTool, SearchWorkspaceTool, WriteFileTool.
- Establish ToolRegistry for managing and executing tools.
- Define types for chat messages, tool calls, and execution results.
- Set up workspace path utilities for file management and validation.
- Configure TypeScript settings for the project.
2025-11-28 16:49:46 +00:00
Simon Larsen
26ac698cc7 Remove Copilot package configuration files 2025-11-28 15:43:36 +00:00
Simon Larsen
72bb25e036 chore: migrate VERSION_PREFIX to VERSION and update related workflows 2025-11-28 15:40:24 +00:00
Nawaz Dhandala
1f23742c1f chore: remove vscode-copilot-chat subproject 2025-11-28 14:12:12 +00:00
Nawaz Dhandala
ac66cee4aa feat: add declaredAt field to Incident model with migration and default value 2025-11-28 10:12:43 +00:00
Nawaz Dhandala
66efe2d2fa feat: add declaredAt field to Incident model and update related services and components 2025-11-28 10:10:05 +00:00
Nawaz Dhandala
0ad5c14882 feat: refactor SCIM creation in TeamMemberService tests for improved clarity 2025-11-27 14:13:14 +00:00
Nawaz Dhandala
2468b39dd2 style: format code for improved readability in TeamMemberService 2025-11-27 13:59:12 +00:00
Nawaz Dhandala
4fec2caef6 feat: update SCIM integration to manage team members with Push Groups 2025-11-27 13:58:47 +00:00
Nawaz Dhandala
dc041d924a style: update social media icons in blog post for improved accessibility 2025-11-27 12:50:30 +00:00
Nawaz Dhandala
37acc617a0 style: add styling for inline code chips in blog body 2025-11-27 12:42:54 +00:00
Nawaz Dhandala
cd28370ce3 style: update color scheme for blog post elements 2025-11-27 12:39:45 +00:00
Nawaz Dhandala
e847f430f2 feat: enhance blog post styling and add reading progress indicator 2025-11-27 12:37:19 +00:00
Nawaz Dhandala
d1e94daaca style: adjust margins for blog post body text 2025-11-27 12:17:10 +00:00
Nawaz Dhandala
df264d6766 feat: add Dockerfile language support to syntax highlighting 2025-11-27 12:16:29 +00:00
Nawaz Dhandala
49c2312c47 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-27 11:30:01 +00:00
Nawaz Dhandala
0fd3121b29 chore: automate version prefix bump and PR creation in release workflow 2025-11-27 11:29:30 +00:00
Simon Larsen
ea43c43991 Merge pull request #2140 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-27 11:20:26 +00:00
simlarsen
51a128efd3 chore: npm audit fix 2025-11-27 01:46:46 +00:00
Nawaz Dhandala
847bac5c6a refactor: enhance Chrome and Firefox executable path retrieval with additional candidates 2025-11-26 16:42:10 +00:00
Nawaz Dhandala
29b137afbd refactor: enhance Chrome executable path retrieval with multiple candidate checks 2025-11-26 16:35:30 +00:00
Nawaz Dhandala
1be0b475a6 bump: update version to 9.1.1 2025-11-26 16:28:59 +00:00
Nawaz Dhandala
2467d2c02d refactor: reorder app dependency installation and Playwright browser setup in Dockerfile 2025-11-26 16:26:17 +00:00
Nawaz Dhandala
b9597250ac refactor: simplify APP_TAG assignment in release and test workflows 2025-11-26 10:58:19 +00:00
Nawaz Dhandala
203e9b8c39 chore: upgrade Docker setup actions to v3 in release and test workflows 2025-11-25 21:41:37 +00:00
Nawaz Dhandala
16078ffe3b Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-25 19:54:54 +00:00
Nawaz Dhandala
898c4de78f refactor: enhance subdomain handling and validation in StatusPageDomain and Domains components 2025-11-25 19:54:50 +00:00
Simon Larsen
da53b7c51c feat: add check for existing GitHub release to skip creation and publishing 2025-11-25 19:51:10 +00:00
Nawaz Dhandala
8a330e7914 refactor: implement caching for Bot Framework adapter to improve performance 2025-11-25 18:51:03 +00:00
Nawaz Dhandala
8bf7b8dfa2 feat: add Microsoft Teams app tenant ID configuration 2025-11-25 18:49:26 +00:00
Nawaz Dhandala
9d36920477 Refactor versioning in release and test workflows to remove build number suffix
- Updated versioning in release.yml to use only major_minor version for Helm chart packaging, Docker images, and GitHub releases.
- Adjusted versioning in test-release.yaml to reflect similar changes, removing build number suffix for test releases.
2025-11-25 13:08:34 +00:00
Nawaz Dhandala
264cdc7c6b refactor: improve code readability by formatting long lines in user update handlers 2025-11-25 11:58:08 +00:00
Nawaz Dhandala
3d8daa46aa refactor: streamline user update logic by consolidating PUT and PATCH handlers 2025-11-25 11:57:42 +00:00
Nawaz Dhandala
673ab6845f refactor: consolidate user update logic into a single handler for PUT and PATCH endpoints 2025-11-25 11:52:43 +00:00
Simon Larsen
bb3df528cf Merge pull request #2137 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-25 08:22:21 +00:00
simlarsen
f52e73afb2 chore: npm audit fix 2025-11-25 01:48:46 +00:00
Nawaz Dhandala
3e04d38eb1 fix: update secret usage documentation and enhance monitor secret retrieval logic 2025-11-24 21:30:08 +00:00
Nawaz Dhandala
27c2ffdfbd chore: remove outdated APK build workflow and script 2025-11-24 21:22:17 +00:00
Nawaz Dhandala
78ee52fb4d fix: add missing iarc_rating_id to the manifest file 2025-11-24 21:08:26 +00:00
Nawaz Dhandala
adc15561b9 feat: enhance SMTP transport configuration by adding flexible secure option handling 2025-11-24 21:04:48 +00:00
Nawaz Dhandala
e19a14e906 fix: update proxy_pass path for assetlinks.json in Nginx configuration 2025-11-24 20:19:49 +00:00
Simon Larsen
035f3412b8 Merge pull request #2132 from OneUptime/captcha
Captcha
2025-11-24 19:13:34 +00:00
Nawaz Dhandala
deb902463c feat: enhance captcha integration by improving type definitions and refactoring callback functions 2025-11-24 19:11:21 +00:00
Nawaz Dhandala
a03a2bf9b0 fix: update proxy_pass path for assetlinks.json in Nginx configuration 2025-11-24 19:06:01 +00:00
Nawaz Dhandala
5f396d36a4 feat: implement assetlinks.json for Android app delegation and enhance Nginx configuration for asset handling 2025-11-24 18:59:42 +00:00
Simon Larsen
99cf626d7d Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-11-24 15:47:59 +00:00
Simon Larsen
ae72437591 fix: update test-e2e workflows to include build number and version dependencies 2025-11-24 15:47:37 +00:00
Simon Larsen
86301213f0 fix: sanitize APP_TAG format by replacing '+' with '-' 2025-11-24 15:42:25 +00:00
Nawaz Dhandala
c6e889b2a8 feat: integrate captcha verification in login process 2025-11-24 15:17:31 +00:00
Nawaz Dhandala
0a053c51e3 feat: update billing payment method permissions to include Manage Billing access 2025-11-24 13:01:08 +00:00
Nawaz Dhandala
296ecbd9e3 feat: enhance error handling in certificate ordering process based on billing status 2025-11-24 12:47:31 +00:00
Nawaz Dhandala
aa4797cc54 fix: add space in subscription management email subject 2025-11-24 12:37:23 +00:00
Nawaz Dhandala
fd4759f16e feat: add site key configuration for hCaptcha in values.yaml 2025-11-24 12:28:05 +00:00
Nawaz Dhandala
a7b7dc61cf feat: add captcha configuration and environment variables to Helm chart and Docker Compose 2025-11-24 12:18:34 +00:00
Nawaz Dhandala
3b0bdca980 feat: implement captcha verification in registration process 2025-11-24 12:13:15 +00:00
Nawaz Dhandala
07bc6d4edd fix: remove public create permission from User table access control 2025-11-24 12:08:33 +00:00
Nawaz Dhandala
8642a54fec feat: add captcha configuration and verification support 2025-11-24 12:08:09 +00:00
Simon Larsen
9ed0c3cf2b Merge pull request #2130 from OneUptime/master
Release
2025-11-24 12:03:48 +00:00
Simon Larsen
396c73f601 Merge pull request #2128 from OneUptime/snyk-upgrade-f06191ee357ef468242a37c903b4b224
[Snyk] Upgrade axios from 1.13.0 to 1.13.1
2025-11-24 12:00:50 +00:00
snyk-bot
ceb54ae12d fix: upgrade axios from 1.13.0 to 1.13.1
Snyk has created this PR to upgrade axios from 1.13.0 to 1.13.1.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/49c81d9c-12c2-4e8e-b9e8-72f98b1b595c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-24 09:37:36 +00:00
Nawaz Dhandala
8df9a14b13 fix: standardize branch name formatting in GitHub Actions workflow 2025-11-22 16:27:47 +00:00
Nawaz Dhandala
7d32627917 Update GitHub Actions workflow for versioning and Docker image builds
- Enhanced the `test-release.yaml` workflow to read and determine semantic versioning from `VERSION_PREFIX`, including major, minor, and patch components.
- Adjusted versioning format in the workflow to use a new scheme: `major.minor.patch-test+build.build_number`.
- Updated Docker image build script to sanitize version strings by replacing '+' with '-' for tagging.
- Incremented the version in `VERSION_PREFIX` from `9.0` to `9.1.0`.
2025-11-22 14:11:02 +00:00
Nawaz Dhandala
a9ea19507e fix: add missing widgets array in manifest.json 2025-11-22 14:05:15 +00:00
Nawaz Dhandala
8c2c002382 fix: update manifest.json to include scope_extensions and adjust client_mode format 2025-11-22 13:52:29 +00:00
Simon Larsen
2a2aca032e Merge pull request #2127 from OneUptime/snyk-upgrade-51c02a535e494371cbaf2b8819deeaff
[Snyk] Upgrade eslint-plugin-unused-imports from 4.2.0 to 4.3.0
2025-11-22 13:00:01 +00:00
snyk-bot
911fe180ab fix: upgrade eslint-plugin-unused-imports from 4.2.0 to 4.3.0
Snyk has created this PR to upgrade eslint-plugin-unused-imports from 4.2.0 to 4.3.0.

See this package in npm:
eslint-plugin-unused-imports

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/c3622982-05c8-495c-809c-20f301c75f92?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-22 10:06:29 +00:00
Simon Larsen
11cbe5f34a Merge pull request #2126 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-22 04:23:48 +00:00
simlarsen
883f51e2d2 chore: npm audit fix 2025-11-22 01:43:11 +00:00
Simon Larsen
8ebe034f0c fix: specify branches for push event in test-release workflow 2025-11-21 19:44:31 +00:00
Simon Larsen
96ea2f6ff7 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-21 19:15:53 +00:00
Simon Larsen
d91a8cae67 chore: remove dashboard Android APK release job from workflow 2025-11-21 19:15:50 +00:00
Simon Larsen
959035fd14 Merge pull request #2124 from OneUptime/sp-master-pw
Sp master pw
2025-11-21 19:14:40 +00:00
Nawaz Dhandala
e1730e4d3a feat: enhance error handling and display for status page and master password components 2025-11-21 19:13:02 +00:00
Nawaz Dhandala
4aa009f46c refactor: clean up code formatting and improve readability in multiple files 2025-11-21 19:07:30 +00:00
Simon Larsen
1551401fc3 fix: disable pipefail for sdkmanager commands to prevent pipeline breakage 2025-11-21 19:02:28 +00:00
Nawaz Dhandala
3768c95aa1 feat: update description for master password rotation to include authentication impact 2025-11-21 18:59:32 +00:00
Nawaz Dhandala
57e933ee9c feat: add master password status alerts in PrivateUser and SSO pages 2025-11-21 18:55:43 +00:00
Nawaz Dhandala
add0efa6db feat: remove master password cookie on user logout 2025-11-21 18:42:19 +00:00
Simon Larsen
7d12c8997e docs: update upgrading guide to reflect removal of Kubernetes Ingress support 2025-11-21 18:42:00 +00:00
Simon Larsen
f836369c01 feat: remove Kubernetes Ingress support and update related documentation 2025-11-21 18:40:47 +00:00
Nawaz Dhandala
f48c3c608c feat: add master password storage key retrieval for user logout process 2025-11-21 18:25:04 +00:00
Simon Larsen
58a955baf7 chore: remove deprecated iOS dashboard build workflow 2025-11-21 18:14:02 +00:00
Simon Larsen
b5243cec1a fix: correct script path casing in APK build workflows 2025-11-21 18:05:10 +00:00
Nawaz Dhandala
d6336ee8f3 refactor: streamline storage key retrieval for private status page and master password requirements 2025-11-21 18:01:01 +00:00
Nawaz Dhandala
1c5506f4d1 fix: reorder access check logic in hasReadAccess method 2025-11-21 17:50:28 +00:00
Simon Larsen
31106c66d5 feat: add workflow to build and upload Dashboard Android APK on release 2025-11-21 17:47:39 +00:00
Nawaz Dhandala
bf33a5ce5d fix: correct syntax in respondWithMasterPasswordAccess function declaration 2025-11-21 17:41:26 +00:00
Simon Larsen
a9f53ec416 fix: restore signing key environment variables in APK build workflow 2025-11-21 17:41:10 +00:00
Nawaz Dhandala
64f819e0db feat: implement master password session validation and response handling 2025-11-21 17:38:07 +00:00
Nawaz Dhandala
e8816d61b0 fix: replace NotAuthenticatedException with BadDataException for invalid master password 2025-11-21 17:25:15 +00:00
Simon Larsen
42713843f3 fix: update manifest URLs and host name in dashboard APK build workflow 2025-11-21 17:20:34 +00:00
Simon Larsen
db8e23c8dc feat: streamline APK build process by introducing a dedicated build script 2025-11-21 17:19:52 +00:00
Nawaz Dhandala
c94430aabe fix: handle API response failure in master password submission 2025-11-21 17:15:47 +00:00
Simon Larsen
e8f74d0147 chore: simplify workflow triggers and environment variables in dashboard build scripts 2025-11-21 17:05:55 +00:00
Nawaz Dhandala
cda3be805b Merge branch 'master' into sp-master-pw 2025-11-21 16:40:19 +00:00
Simon Larsen
372ce67ce6 Refactor Docker image build process in GitHub Actions workflow
- Replaced inline Docker build commands with a dedicated script for building Docker images.
- Added NPM_AUTH_TOKEN environment variable for authentication.
- Updated image build commands for multiple services (mcp-server, llm, nginx, e2e, test-server, otel-collector, isolated-vm, home, status-page, test, probe-ingest, server-monitor-ingest, incoming-request-ingest, telemetry, probe, dashboard, admin-dashboard, app, api-reference, accounts, worker, copilot, workflow, docs) to use the new script.
2025-11-21 14:57:15 +00:00
Simon Larsen
62cae0d32c feat: add build number generation and version reading for test releases 2025-11-21 13:26:42 +00:00
Simon Larsen
6be92aa41d Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-21 13:22:52 +00:00
Simon Larsen
d166fe49ec Implement feature X to enhance user experience and optimize performance 2025-11-21 13:22:46 +00:00
Simon Larsen
d5f2b32fe9 Merge pull request #2125 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-21 12:53:53 +00:00
simlarsen
be71858b4a chore: npm audit fix 2025-11-21 01:46:46 +00:00
Simon Larsen
8c121869ee Add GitHub Actions workflow for building Dashboard iOS Wrapper 2025-11-20 21:11:35 +00:00
Simon Larsen
bfc761aac5 Add GitHub Actions workflow for building Dashboard Android APK 2025-11-20 21:08:18 +00:00
Simon Larsen
17f6507d0c Add build_docker_images.sh script for automated Docker image builds
- Implemented a bash script to build and push Docker images.
- Added support for multiple platforms using docker buildx.
- Included options for specifying image name, version, Dockerfile path, build context, and additional tags.
- Integrated Git SHA detection for build arguments.
- Provided usage instructions and error handling for missing required arguments.
2025-11-20 20:51:11 +00:00
Simon Larsen
4fa4dd7b6c Refactor Docker image build process to use a dedicated script for improved maintainability 2025-11-20 20:23:41 +00:00
Simon Larsen
69be00067a Add 'set -euo pipefail' to Docker build commands for improved error handling 2025-11-20 19:43:44 +00:00
Nawaz Dhandala
7af0091de2 Refactor master password validation logic to support status page-specific keys and handle legacy values 2025-11-20 19:31:41 +00:00
Simon Larsen
cd6bac1111 Update screenshots in manifest.json for improved app representation on desktop and mobile 2025-11-20 19:07:24 +00:00
Nawaz Dhandala
62578b2389 Rename package from @oneuptime/probe-ingest to @oneuptime/telemetry 2025-11-20 19:05:48 +00:00
Simon Larsen
0a791bba01 Refactor code structure for improved readability and maintainability 2025-11-20 18:59:40 +00:00
Nawaz Dhandala
accd86edf1 Refactor getLoginRoute to handle master password validation for private status pages 2025-11-20 18:42:30 +00:00
Nawaz Dhandala
8f3c06bc86 Refactor MasterPasswordPage to use BasicForm for password submission and improve error handling 2025-11-20 18:30:20 +00:00
Nawaz Dhandala
7baeaaee02 Add master password route check in DashboardMasterPage component 2025-11-20 18:06:19 +00:00
Nawaz Dhandala
8f05de6860 Add MasterPassword component and integrate master password checks in Login and SSO pages 2025-11-20 18:03:22 +00:00
Nawaz Dhandala
05e0c5528e Refactor StatusPageDelete component to reintroduce 'Require Master Password' toggle for private status pages 2025-11-20 14:21:11 +00:00
Nawaz Dhandala
91ef08595c Update read permissions for StatusPage to include necessary roles 2025-11-20 13:54:56 +00:00
Nawaz Dhandala
04459b51da Add master password rotation feature to StatusPageDelete component 2025-11-20 13:53:13 +00:00
Nawaz Dhandala
5903764395 Refactor MasterPasswordPage for improved readability and functionality 2025-11-20 13:08:06 +00:00
Simon Larsen
51ac869650 Fix formatting in manifest.json by removing unnecessary blank line 2025-11-20 13:07:40 +00:00
Simon Larsen
ce4a49fbd2 Fix app ID in manifest.json for correct routing 2025-11-20 12:59:28 +00:00
Simon Larsen
ca252d8e64 Add screenshots to manifest.json for enhanced app visibility 2025-11-20 12:59:16 +00:00
Nawaz Dhandala
850d125c82 Add migration for master password feature in StatusPage 2025-11-20 12:56:27 +00:00
Nawaz Dhandala
4b619eadc0 Implement master password feature for private status pages 2025-11-20 12:51:11 +00:00
Simon Larsen
0fc63385a5 Remove unused manifest files and service workers across multiple components for cleaner codebase 2025-11-20 12:34:20 +00:00
Simon Larsen
b5c0953c8b Merge pull request #2123 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-20 12:00:36 +00:00
Nawaz Dhandala
9deaf19d4c Remove unnecessary blank lines in config.example.env for improved readability 2025-11-20 11:59:56 +00:00
simlarsen
ac239ffe4d chore: npm audit fix 2025-11-20 01:45:52 +00:00
Simon Larsen
84ac063445 Merge pull request #2122 from OneUptime/master
Release
2025-11-19 18:29:37 +00:00
Nawaz Dhandala
904311b124 Add modal width configuration to ChangeState components for improved UI consistency 2025-11-19 18:26:34 +00:00
Nawaz Dhandala
c1562872a0 Fix formatting of response snapshot section in MonitorCriteriaEvaluator 2025-11-19 18:22:04 +00:00
Nawaz Dhandala
503d5ff946 Refactor MonitorCriteriaEvaluator to improve code readability and consistency 2025-11-19 18:18:56 +00:00
Nawaz Dhandala
271fa4c9ad Add root cause context building and enhance monitor criteria evaluation details 2025-11-19 18:18:25 +00:00
Nawaz Dhandala
a57902dd07 Fix indentation for detailsUrl in Service class to maintain code consistency 2025-11-19 18:00:35 +00:00
Nawaz Dhandala
9336a33e47 Enhance notification templates to include conditional details URLs for improved user guidance 2025-11-19 18:00:08 +00:00
Nawaz Dhandala
fe034807d7 Update PermissionHelper to mark dashboard permissions as access control permissions 2025-11-19 17:43:52 +00:00
Nawaz Dhandala
f08adc7b78 Refactor ScheduledMaintenanceService and AnnouncementView to improve notification settings handling and enhance code clarity 2025-11-19 17:43:01 +00:00
Nawaz Dhandala
e767b3f4b7 Refactor ScheduledMaintenanceService and related migrations to improve notification settings handling and enhance code clarity 2025-11-19 17:42:37 +00:00
Nawaz Dhandala
c874f4f3e2 Refactor Announcement components to enhance breadcrumb navigation and improve layout 2025-11-19 17:28:01 +00:00
Nawaz Dhandala
cb3fb984ec Refactor AnnouncementView to enhance attachment handling and improve content display 2025-11-19 17:19:34 +00:00
Nawaz Dhandala
51882a595a Refactor OpenAPIUtil to streamline permission checks and improve schema registration logic 2025-11-19 17:07:37 +00:00
Nawaz Dhandala
8d5f8454c4 Refactor FilePicker and AttachmentList components to enhance type definitions and improve code clarity 2025-11-19 16:02:41 +00:00
Nawaz Dhandala
a036830009 Refactor Response and FilePicker components for improved readability and consistency 2025-11-19 15:50:45 +00:00
Nawaz Dhandala
76c6ffeb51 Refactor AttachmentCard component for improved styling and layout 2025-11-19 15:37:17 +00:00
Nawaz Dhandala
92c9de7ca9 Add EventAttachmentList component for improved attachment display in EventItem 2025-11-19 15:34:50 +00:00
Nawaz Dhandala
e752340a16 Refactor FilePicker component to remove thumbnail generation and simplify file display 2025-11-19 14:33:37 +00:00
Nawaz Dhandala
3f7d7d4347 Refactor FilePicker component to improve file size formatting and enhance metadata display 2025-11-19 14:29:00 +00:00
Nawaz Dhandala
30651f1ca7 Update attachment routes to include projectId in URL parameters 2025-11-19 13:17:48 +00:00
Nawaz Dhandala
a04fa51c37 Add UserMiddleware for authorization in attachment routes 2025-11-19 12:47:02 +00:00
Nawaz Dhandala
5c5a4ca787 Refactor AttachmentList component styles for improved accessibility and visual consistency 2025-11-19 12:13:11 +00:00
Nawaz Dhandala
62fe797b93 Enhance AttachmentList component with file metadata and icon support 2025-11-19 12:07:39 +00:00
Nawaz Dhandala
1ee2f17b28 Add canReadOnRelationQuery property to _id in DatabaseBaseModel 2025-11-19 11:52:39 +00:00
Nawaz Dhandala
9e714af5c2 Merge branch 'file-attachments' 2025-11-19 10:45:29 +00:00
Simon Larsen
74e18a2861 Merge pull request #2113 from OneUptime/file-attachments
File attachments
2025-11-19 10:21:46 +00:00
Simon Larsen
cd9b3c1386 Merge pull request #2115 from OneUptime/snyk-upgrade-fc65b788097a8d536953d6703a423b18
[Snyk] Upgrade tailwind-merge from 2.5.4 to 2.6.0
2025-11-19 10:21:28 +00:00
Simon Larsen
40a7fc5d02 Merge pull request #2116 from OneUptime/snyk-upgrade-263ef7972ec4039196a129208a656e3e
[Snyk] Upgrade pg from 8.13.1 to 8.16.3
2025-11-19 10:21:21 +00:00
Simon Larsen
49ccb8fd75 Merge pull request #2117 from OneUptime/snyk-upgrade-f6a1bcc29ab8dd02d9ccadbb68709ae9
[Snyk] Upgrade react-big-calendar from 1.15.0 to 1.19.4
2025-11-19 10:21:14 +00:00
Simon Larsen
5c3923f534 Merge pull request #2118 from OneUptime/snyk-upgrade-17aece182fff53ced7b2e67a281299a5
[Snyk] Upgrade @opentelemetry/semantic-conventions from 1.27.0 to 1.37.0
2025-11-19 10:21:06 +00:00
Simon Larsen
7a94684cec Merge pull request #2119 from OneUptime/snyk-upgrade-412cabf449455ad4dfe0f5c97f262cd4
[Snyk] Upgrade react-router-dom from 6.28.0 to 6.30.1
2025-11-19 10:21:01 +00:00
Simon Larsen
893ccf3331 Merge pull request #2114 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-19 10:20:41 +00:00
snyk-bot
b5f354da75 fix: upgrade react-router-dom from 6.28.0 to 6.30.1
Snyk has created this PR to upgrade react-router-dom from 6.28.0 to 6.30.1.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-19 03:38:55 +00:00
snyk-bot
b128be0c13 fix: upgrade @opentelemetry/semantic-conventions from 1.27.0 to 1.37.0
Snyk has created this PR to upgrade @opentelemetry/semantic-conventions from 1.27.0 to 1.37.0.

See this package in npm:
@opentelemetry/semantic-conventions

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-19 03:38:52 +00:00
snyk-bot
a0fae9b514 fix: upgrade react-big-calendar from 1.15.0 to 1.19.4
Snyk has created this PR to upgrade react-big-calendar from 1.15.0 to 1.19.4.

See this package in npm:
react-big-calendar

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-19 03:38:47 +00:00
snyk-bot
04fc51b873 fix: upgrade pg from 8.13.1 to 8.16.3
Snyk has created this PR to upgrade pg from 8.13.1 to 8.16.3.

See this package in npm:
pg

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-19 03:38:43 +00:00
snyk-bot
2cb45d18c9 fix: upgrade tailwind-merge from 2.5.4 to 2.6.0
Snyk has created this PR to upgrade tailwind-merge from 2.5.4 to 2.6.0.

See this package in npm:
tailwind-merge

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-19 03:38:39 +00:00
simlarsen
495f59f36b chore: npm audit fix 2025-11-19 01:47:18 +00:00
Nawaz Dhandala
21da644555 feat: add file size validation to FilePicker component 2025-11-18 20:49:58 +00:00
Nawaz Dhandala
02e7bd6f3e feat: enhance file summary rendering with subtitles and access labels 2025-11-18 20:43:04 +00:00
Nawaz Dhandala
254795261d feat: implement file summary rendering in FormSummary component 2025-11-18 20:10:17 +00:00
Nawaz Dhandala
53f0cd144c feat: prevent notifying subscribers when sending a test email directly 2025-11-18 19:49:01 +00:00
Nawaz Dhandala
679864dbaa feat: enhance FilePicker component with upload status management and progress tracking 2025-11-18 19:43:40 +00:00
Nawaz Dhandala
c6acc85d7d feat: refactor cache header handling to use Response utility method across multiple APIs 2025-11-18 19:26:18 +00:00
Nawaz Dhandala
5ea2426ea4 feat: enhance FilePicker component UI by updating file upload labels and error display 2025-11-18 19:03:07 +00:00
Nawaz Dhandala
1144783f4d feat: update button styles in FilePicker component for improved visibility 2025-11-18 18:54:51 +00:00
Nawaz Dhandala
d68bc56d1b feat: add remove button for files in FilePicker component 2025-11-18 18:50:51 +00:00
Nawaz Dhandala
5170254473 feat: improve file handling in FilePicker for better multi-file support and UI enhancements 2025-11-18 18:37:51 +00:00
Nawaz Dhandala
84353e1a05 feat: update FilePicker component to use full width for better layout 2025-11-18 18:08:39 +00:00
Nawaz Dhandala
62d74c1d84 feat: enhance file handling by adding MIME type resolution and updating file creation permissions 2025-11-18 18:05:46 +00:00
Nawaz Dhandala
654f64aaf7 feat: add attachment support to notes in Alerts, Incidents, and Scheduled Maintenance 2025-11-18 16:15:43 +00:00
Nawaz Dhandala
150af5b65d feat: enhance file attachment handling across multiple APIs and services 2025-11-18 15:59:58 +00:00
Nawaz Dhandala
3740636136 feat: add migration for AlertInternalNoteFile table and update migration index references 2025-11-18 15:50:58 +00:00
Nawaz Dhandala
d40deae7ef feat: add migration for StatusPageAnnouncementFile and AlertInternalNoteFile tables with constraints and indexes 2025-11-18 15:49:40 +00:00
Nawaz Dhandala
669ed24249 feat: implement AttachmentList component and refactor attachment rendering in various views 2025-11-18 15:47:19 +00:00
Nawaz Dhandala
ae341eae08 refactor: clean up import statements and improve code readability across multiple files 2025-11-18 15:32:17 +00:00
Nawaz Dhandala
241ff7671d feat: add file attachment support to status page announcements, including API endpoints and UI updates 2025-11-18 15:30:56 +00:00
Nawaz Dhandala
b7492f0706 feat: implement file attachment functionality for internal notes, including API endpoints and UI integration 2025-11-18 15:11:16 +00:00
Nawaz Dhandala
fcd076d057 feat: add migration for ScheduledMaintenanceInternalNoteFile and ScheduledMaintenancePublicNoteFile tables 2025-11-18 14:53:19 +00:00
Nawaz Dhandala
b924d68c51 feat: implement file attachment support for scheduled maintenance notes and API endpoints 2025-11-18 14:52:31 +00:00
Nawaz Dhandala
4713a42829 feat: add migration for IncidentInternalNoteFile and IncidentPublicNoteFile tables 2025-11-18 13:17:25 +00:00
Nawaz Dhandala
9399907bfc feat: add attachment handling to incident public notes in event timeline 2025-11-18 13:13:37 +00:00
Nawaz Dhandala
2a84fd6751 feat: add endpoint to retrieve incident public note attachments 2025-11-18 12:04:50 +00:00
Nawaz Dhandala
4efd3d0428 feat: add file attachment rendering to IncidentDelete and PublicNote components 2025-11-18 11:38:38 +00:00
Simon Larsen
535ae01dee Merge pull request #2112 from OneUptime/snyk-fix-645fef7c043ef7c5c2bc5ebfdc48dccb
[Snyk] Security upgrade xmlbuilder2 from 3.1.1 to 4.0.0
2025-11-18 11:29:33 +00:00
Nawaz Dhandala
b04b59b0a9 feat: add support for multiple file attachments in IncidentFeed form 2025-11-18 10:42:38 +00:00
snyk-bot
92025ce415 fix: Home/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-JSYAML-13961110
2025-11-18 10:35:30 +00:00
Nawaz Dhandala
4893b01f38 feat: implement file attachment handling in IncidentInternalNote and IncidentPublicNote services 2025-11-18 10:33:00 +00:00
Nawaz Dhandala
0f8436b92f feat: enhance FormField to support multiple file uploads 2025-11-18 10:26:14 +00:00
Nawaz Dhandala
5e4aa44f2a feat: add file attachment support to IncidentInternalNote and IncidentPublicNote APIs 2025-11-18 10:20:07 +00:00
Simon Larsen
0290355cfc Merge pull request #2111 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-18 09:49:22 +00:00
simlarsen
64360fc3fe chore: npm audit fix 2025-11-18 01:47:15 +00:00
Simon Larsen
dd07cb6312 Merge pull request #2110 from OneUptime/incidents-attachments
Incidents attachments
2025-11-17 15:58:11 +00:00
Nawaz Dhandala
46e5aae8e4 fix: update path resolution for blank profile picture 2025-11-17 15:57:49 +00:00
Nawaz Dhandala
626b6d93a8 fix: update path resolution for blank profile picture 2025-11-17 15:46:41 +00:00
Nawaz Dhandala
e570030319 fix: correct import path for AppApiRoute in User utility 2025-11-17 15:38:56 +00:00
Nawaz Dhandala
97d3a34abc refactor: remove permissions from TableAccessControl in File model 2025-11-17 14:43:01 +00:00
Nawaz Dhandala
848c441419 refactor: simplify API route attachment and improve error handling in UserAPI 2025-11-17 14:41:02 +00:00
Nawaz Dhandala
5b7e52a94e feat: enhance user profile picture handling with improved blank profile logic and route management 2025-11-17 14:35:03 +00:00
Nawaz Dhandala
77e3394638 feat: update user profile picture handling to use userId instead of profilePictureId 2025-11-17 14:21:33 +00:00
Nawaz Dhandala
93721350c6 feat: update logo URL handling and introduce UserAPI for profile picture management 2025-11-17 14:09:38 +00:00
Nawaz Dhandala
1bc6eca55c refactor: streamline logo URL route handling in various components 2025-11-17 13:56:40 +00:00
Nawaz Dhandala
29560e3a4a feat: update logo URL handling for status pages across various services 2025-11-17 13:45:36 +00:00
Simon Larsen
03cc76ab07 Merge pull request #2109 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-15 07:56:37 +00:00
simlarsen
dade1d0403 chore: npm audit fix 2025-11-15 01:44:36 +00:00
Nawaz Dhandala
8a3feab3d0 feat: enhance number formatting in CompareCriteria class 2025-11-13 15:10:17 +00:00
Nawaz Dhandala
7864bbb87b feat: simplify telemetry monitor type check in SummaryInfo component 2025-11-13 14:55:44 +00:00
Nawaz Dhandala
d112d87b80 feat: add ObjectID type for projectId in ExceptionInstanceTable component 2025-11-13 14:35:58 +00:00
Nawaz Dhandala
2f8fcabce4 refactor: clean up code formatting and improve readability in Telemetry and ExceptionInstanceTable components 2025-11-13 14:35:36 +00:00
Nawaz Dhandala
0023560588 feat: enhance error handling with auth refresh logic in API class 2025-11-13 14:35:00 +00:00
Nawaz Dhandala
0bc14acde9 feat: migrate exception monitors to ExceptionInstance (analytics)
- Replace TelemetryException with ExceptionInstance across types and telemetry query
- Rename MonitorStepExceptionMonitorUtil.toQuery -> toAnalyticsQuery and map fields (telemetryServiceId->serviceId, lastSeenAt->time)
- Use ExceptionInstanceService and Query<ExceptionInstance> in MonitorTelemetryMonitor
- Minor MonitorResource variable cleanup for ExceptionMonitorResponse
- Add ExceptionInstanceTable component and switch UI (forms, Alert/Incident views) to use ExceptionInstance
2025-11-13 14:33:01 +00:00
Nawaz Dhandala
3f3956edd6 feat: implement SDK-side aggregation selector for modern metrics API 2025-11-13 13:32:35 +00:00
Nawaz Dhandala
93755da2e8 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-13 12:06:46 +00:00
Nawaz Dhandala
0657222ea7 Add support for Exception monitor type in MonitorCriteriaInstance 2025-11-13 12:06:42 +00:00
Nawaz Dhandala
ca352826ca Add Exception Monitor functionality with related types and components 2025-11-13 12:02:17 +00:00
Simon Larsen
3cbd99042b Merge pull request #2107 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-13 09:38:52 +00:00
simlarsen
2f102acdc2 chore: npm audit fix 2025-11-13 01:48:58 +00:00
Nawaz Dhandala
b76811d152 Fix indentation for headers in API requests across multiple components 2025-11-12 17:55:54 +00:00
Nawaz Dhandala
2335935a3e Add conditional rendering for app.enabled in app.yaml 2025-11-12 17:53:01 +00:00
Nawaz Dhandala
c324fe03d3 Update API header retrieval to remove dependency on StatusPageUtil for all subscription and announcement pages 2025-11-12 17:52:44 +00:00
Nawaz Dhandala
d5bc83a5a1 Remove deprecated @opentelemetry/sdk-trace-node dependency from package.json and package-lock.json 2025-11-12 17:44:28 +00:00
Nawaz Dhandala
e2baa449f5 Refactor Telemetry initialization to explicitly type webTraceExporter as WebSpanExporter 2025-11-12 16:18:39 +00:00
Nawaz Dhandala
51b88eb065 Refactor Telemetry class to streamline span processor registration 2025-11-12 16:16:34 +00:00
Nawaz Dhandala
b0d95bb7df Add 'enabled' property to various components in values schema and YAML templates 2025-11-12 16:15:29 +00:00
Nawaz Dhandala
8bf8c891ab Refactor telemetry exports to improve type handling and streamline initialization 2025-11-12 13:32:27 +00:00
Nawaz Dhandala
fcf919c70b Refactor session handling and cookie management in StatusPage authentication 2025-11-12 12:52:19 +00:00
Nawaz Dhandala
f0f3d32d31 Merge remote-tracking branch 'origin/snyk-upgrade-b70b2734abb0e16d5d110c8cd2735c35' 2025-11-12 11:49:20 +00:00
Nawaz Dhandala
444e8f17b6 Implement feature X to enhance user experience and fix bug Y in module Z 2025-11-12 11:46:44 +00:00
Nawaz Dhandala
3aabf44b4e Merge branch 'snyk-upgrade-f017994c6dac770941ee664640830ac7' 2025-11-12 11:45:47 +00:00
Simon Larsen
c11fcc3c8e Merge pull request #2106 from OneUptime/user-refresh-token
User refresh token
2025-11-12 11:42:29 +00:00
Simon Larsen
52519c9af8 Merge pull request #2104 from OneUptime/snyk-upgrade-d659682fbd62c498810b328f5aaca524
[Snyk] Upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0
2025-11-12 11:42:08 +00:00
Simon Larsen
2483cf9499 Merge pull request #2102 from OneUptime/snyk-upgrade-289d4467acd89e4854c2a2dc61916341
[Snyk] Upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0
2025-11-12 11:41:44 +00:00
Simon Larsen
634e21b13c Merge pull request #2101 from OneUptime/snyk-upgrade-8678075385220dcc2c31b1b4a3900956
[Snyk] Upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0
2025-11-12 11:41:35 +00:00
Nawaz Dhandala
aad933b9eb feat(Authentication, Session Management): implement finalizeStatusPageLogin and refresh-token endpoints for enhanced session handling 2025-11-12 11:40:52 +00:00
Nawaz Dhandala
9356f2964e feat(Authentication): integrate UserSession model and enhance finalizeUserLogin type definition
feat(Express): define HeaderValue type and improve type annotations for headerValueToString and extractDeviceInfo functions
2025-11-11 21:41:20 +00:00
Nawaz Dhandala
aae70ead3b refactor: streamline code formatting and improve readability across multiple files 2025-11-11 21:36:12 +00:00
Nawaz Dhandala
8a482dce10 feat(UserSession): enhance session management with comprehensive session handling methods and metadata 2025-11-11 21:34:11 +00:00
Nawaz Dhandala
9fdf46889c feat(Text): add truncate method for string length limitation 2025-11-11 21:32:57 +00:00
Nawaz Dhandala
40ca9dc04c feat(Authentication, SSO): enhance session management with user session creation and refresh token handling 2025-11-11 21:22:21 +00:00
Nawaz Dhandala
74937f2208 feat(DeviceInfo): add extractDeviceInfo function and RequestDeviceInfo type for enhanced device data retrieval 2025-11-11 21:20:07 +00:00
Nawaz Dhandala
c02ab56477 feat(Cookie): enhance cookie management with refresh token support and default access token expiry 2025-11-11 21:11:34 +00:00
Nawaz Dhandala
3f99b9680f feat(Migration): add UserSession and StatusPagePrivateUserSession migrations with constraints and indexes 2025-11-11 19:51:54 +00:00
Nawaz Dhandala
b08c39037d feat(Index): add StatusPagePrivateUserSessionService and UserSessionService to services 2025-11-11 19:49:32 +00:00
Nawaz Dhandala
f7cc3c00da feat(Migration): add migration for StatusPagePrivateUserSession and UserSession tables 2025-11-11 19:48:42 +00:00
Nawaz Dhandala
ac4286935a refactor(StatusPagePrivateUserSession): remove unnecessary blank line for cleaner code
refactor(UserSession): format description for additional info column for improved readability
2025-11-11 19:47:08 +00:00
Nawaz Dhandala
90a0b2e4a8 refactor(StatusPagePrivateUserSession): simplify access control by removing specific permissions 2025-11-11 19:46:19 +00:00
Nawaz Dhandala
9b22c48d27 feat(UserSession): add UserSession model for managing active user sessions and security tokens 2025-11-11 19:41:41 +00:00
Nawaz Dhandala
9c9dad5da0 feat(UserSettings): add user settings page and integrate into side menu 2025-11-11 19:18:20 +00:00
Nawaz Dhandala
e986f74025 fix(TeamMemberService): skip one-member guard when SCIM manages project membership 2025-11-11 19:07:11 +00:00
snyk-bot
deb2e81b21 fix: upgrade @opentelemetry/exporter-trace-otlp-proto from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-trace-otlp-proto from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-trace-otlp-proto

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:46:09 +00:00
snyk-bot
0f8b322892 fix: upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-trace-otlp-http

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:46:03 +00:00
snyk-bot
23c7de3ecd fix: upgrade @opentelemetry/exporter-metrics-otlp-proto from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-metrics-otlp-proto from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-metrics-otlp-proto

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:57 +00:00
snyk-bot
ad144a6240 fix: upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-logs-otlp-http

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:52 +00:00
snyk-bot
debfef0388 fix: upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/sdk-logs

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:46 +00:00
Nawaz Dhandala
bb85c9f8c8 refactor(BaseModelTable): enhance filter function for dropdown labels to improve type safety 2025-11-11 17:23:14 +00:00
Nawaz Dhandala
25ab1cdbf9 refactor(BaseModelTable): improve code formatting and simplify access control value handling 2025-11-11 17:21:51 +00:00
Nawaz Dhandala
44b8a9ddc9 feat(BaseModelTable): integrate access control column handling and update dropdown labels with color support 2025-11-11 17:16:52 +00:00
Nawaz Dhandala
c388ff9550 fix(IncidentCreate): remove unnecessary whitespace in color mapping function 2025-11-11 15:59:04 +00:00
Nawaz Dhandala
321d1680e6 feat(IncidentCreate): enhance incident state options with color attribute 2025-11-11 15:58:13 +00:00
Nawaz Dhandala
6c0e9f0fed refactor: remove unnecessary whitespace in model classes and improve code formatting 2025-11-11 15:49:20 +00:00
Nawaz Dhandala
99349ecb30 fix: restore @ColorField decorator in multiple model classes for color handling 2025-11-11 15:47:57 +00:00
Nawaz Dhandala
258bbbd9cf feat(BaseModelTable): enhance dropdown options with color handling based on the first color column 2025-11-11 15:37:56 +00:00
Nawaz Dhandala
1094a07fc6 fix(DropdownUtil): ensure color variable is explicitly typed as Color | null
refactor(ColorField): add return type annotation for ColorField function
2025-11-11 14:30:33 +00:00
Nawaz Dhandala
14a5671645 feat(Color, Dropdown, ModelForm): enhance color handling in Color class and Dropdown options 2025-11-11 14:05:30 +00:00
Nawaz Dhandala
5a41c66953 refactor(DatabaseBaseModel, ColorField): improve formatting and readability of type definitions and method implementations 2025-11-11 13:26:02 +00:00
Nawaz Dhandala
af605fce4c feat(DatabaseBaseModel): add getFirstColorColumn method to retrieve the first color field column 2025-11-11 13:23:55 +00:00
Nawaz Dhandala
f8ef6c69fe feat(ColorField): add ColorField decorator and related utility functions for color field management in database models 2025-11-11 13:20:42 +00:00
Simon Larsen
e1848f44f7 Merge pull request #2100 from OneUptime/dropdown-lbl
Dropdown lbl
2025-11-11 12:49:54 +00:00
Nawaz Dhandala
825bd39dda feat(Dropdown): enhance type definitions for styling functions and improve label rendering 2025-11-11 12:49:25 +00:00
Nawaz Dhandala
b99905dfe8 fix(Telemetry): add npm install step for Common directory in workflow 2025-11-11 12:45:06 +00:00
Nawaz Dhandala
a4bf40a2c1 fix(Label): correct indentation for icon property in LabelElement 2025-11-11 12:44:05 +00:00
Nawaz Dhandala
711998b048 feat(Icon): add strokeWidth to EmptyCircle icon rendering 2025-11-11 12:42:36 +00:00
Nawaz Dhandala
132e044c07 feat(Icon): add EmptyCircle icon and update Label to use it 2025-11-11 12:16:01 +00:00
Nawaz Dhandala
8ecc307451 feat(Dropdown): implement label rendering and color resolution for selected labels 2025-11-11 12:02:49 +00:00
Nawaz Dhandala
c85c29989f feat(Dropdown): update styling for focused and selected states, enhance multi-value appearance 2025-11-10 23:29:16 +00:00
Nawaz Dhandala
95726e0f21 feat(Dropdown): enhance dropdown options with label support and improve styling 2025-11-10 23:13:10 +00:00
Nawaz Dhandala
adc15992e9 refactor(Label): simplify import statements for Pill component 2025-11-10 22:49:36 +00:00
Nawaz Dhandala
58d83a2a80 feat(Pill): add thickness prop to Icon component in Pill 2025-11-10 22:48:57 +00:00
Nawaz Dhandala
5461cd4502 feat(Pill): add icon support to Pill component and update tests 2025-11-10 22:44:07 +00:00
Nawaz Dhandala
478465a65b refactor: update label imports and restructure label components for consistency 2025-11-10 22:34:33 +00:00
437 changed files with 38381 additions and 52351 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,5 +17,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Telemetry && npm install && npm run test

View File

@@ -29,25 +29,25 @@
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@elastic/elasticsearch": "^8.12.1",
"@hcaptcha/react-hcaptcha": "^1.14.0",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-fetch": "^0.52.1",
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
"@opentelemetry/instrumentation": "^0.207.0",
"@opentelemetry/instrumentation-fetch": "^0.207.0",
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-logs": "^0.52.1",
"@opentelemetry/sdk-logs": "^0.207.0",
"@opentelemetry/sdk-metrics": "^1.25.1",
"@opentelemetry/sdk-node": "^0.52.1",
"@opentelemetry/sdk-trace-node": "^1.25.1",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -86,21 +86,21 @@
"node-cron": "^3.0.3",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"pg": "^8.16.3",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.13.0",
"react-big-calendar": "^1.19.4",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.2",
"react-error-boundary": "^4.0.13",
"react-highlight": "^0.15.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.24.1",
"react-router-dom": "^6.30.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^16.0.0",
@@ -115,7 +115,7 @@
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
"stripe": "^10.17.0",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.6.0",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.26",
@@ -3107,10 +3107,11 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"

View File

@@ -33,25 +33,25 @@
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@elastic/elasticsearch": "^8.12.1",
"@hcaptcha/react-hcaptcha": "^1.14.0",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-fetch": "^0.52.1",
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
"@opentelemetry/instrumentation": "^0.207.0",
"@opentelemetry/instrumentation-fetch": "^0.207.0",
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-logs": "^0.52.1",
"@opentelemetry/sdk-logs": "^0.207.0",
"@opentelemetry/sdk-metrics": "^1.25.1",
"@opentelemetry/sdk-node": "^0.52.1",
"@opentelemetry/sdk-trace-node": "^1.25.1",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -90,21 +90,21 @@
"node-cron": "^3.0.3",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"pg": "^8.16.3",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.13.0",
"react-big-calendar": "^1.19.4",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.2",
"react-error-boundary": "^4.0.13",
"react-highlight": "^0.15.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.24.1",
"react-router-dom": "^6.30.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^16.0.0",
@@ -119,7 +119,7 @@
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
"stripe": "^10.17.0",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.6.0",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.26",

View File

@@ -7,10 +7,20 @@ import {
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import ModelForm, { FormType } from "Common/UI/Components/Forms/ModelForm";
import ModelForm, {
FormType,
ModelField,
} from "Common/UI/Components/Forms/ModelForm";
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import Link from "Common/UI/Components/Link/Link";
import { DASHBOARD_URL } from "Common/UI/Config";
import Captcha from "Common/UI/Components/Captcha/Captcha";
import {
DASHBOARD_URL,
CAPTCHA_ENABLED,
CAPTCHA_SITE_KEY,
} from "Common/UI/Config";
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
import EditionLabel from "Common/UI/Components/EditionLabel/EditionLabel";
import UiAnalytics from "Common/UI/Utils/Analytics";
@@ -73,6 +83,84 @@ const LoginPage: () => JSX.Element = () => {
const [twofactorAuthError, setTwoFactorAuthError] =
React.useState<string>("");
const isCaptchaEnabled: boolean =
CAPTCHA_ENABLED && Boolean(CAPTCHA_SITE_KEY);
const [shouldResetCaptcha, setShouldResetCaptcha] =
React.useState<boolean>(false);
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
const handleCaptchaReset: () => void = React.useCallback(() => {
setCaptchaResetSignal((current: number) => {
return current + 1;
});
}, []);
let loginFields: Array<ModelField<User>> = [
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: "jeff@example.com",
required: true,
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
disableSpellCheck: true,
},
{
field: {
password: true,
},
title: "Password",
required: true,
validation: {
minLength: 6,
},
fieldType: FormFieldSchemaType.Password,
sideLink: {
text: "Forgot password?",
url: new Route("/accounts/forgot-password"),
openLinkInNewTab: false,
},
dataTestId: "password",
disableSpellCheck: true,
},
];
if (isCaptchaEnabled) {
loginFields = loginFields.concat([
{
overrideField: {
captchaToken: true,
},
overrideFieldKey: "captchaToken",
fieldType: FormFieldSchemaType.CustomComponent,
title: "Human Verification",
description:
"Complete the captcha challenge so we know you're not a bot.",
required: true,
showEvenIfPermissionDoesNotExist: true,
getCustomElement: (
_values: FormValues<User>,
customProps: CustomElementProps,
) => {
return (
<Captcha
siteKey={CAPTCHA_SITE_KEY}
resetSignal={captchaResetSignal}
error={customProps.error}
onTokenChange={(token: string) => {
customProps.onChange?.(token);
}}
onBlur={customProps.onBlur}
/>
);
},
},
]);
}
useAsyncEffect(async () => {
if (Navigation.getQueryStringByName("email")) {
setInitialValues({
@@ -228,45 +316,41 @@ const LoginPage: () => JSX.Element = () => {
modelType={User}
id="login-form"
name="Login"
fields={[
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: "jeff@example.com",
required: true,
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
disableSpellCheck: true,
},
{
field: {
password: true,
},
title: "Password",
required: true,
validation: {
minLength: 6,
},
fieldType: FormFieldSchemaType.Password,
sideLink: {
text: "Forgot password?",
url: new Route("/accounts/forgot-password"),
openLinkInNewTab: false,
},
dataTestId: "password",
disableSpellCheck: true,
},
]}
fields={loginFields}
createOrUpdateApiUrl={apiUrl}
formType={FormType.Create}
submitButtonText={"Login"}
onBeforeCreate={(data: User) => {
onBeforeCreate={(data: User, miscDataProps: JSONObject) => {
if (isCaptchaEnabled) {
const captchaToken: string | undefined = (
miscDataProps["captchaToken"] as string | undefined
)
?.toString()
.trim();
if (!captchaToken) {
throw new Error(
"Please complete the captcha challenge before signing in.",
);
}
miscDataProps["captchaToken"] = captchaToken;
setShouldResetCaptcha(true);
}
setInitialValues(User.toJSON(data, User));
return Promise.resolve(data);
}}
onLoadingChange={(loading: boolean) => {
if (!isCaptchaEnabled) {
return;
}
if (!loading && shouldResetCaptcha) {
setShouldResetCaptcha(false);
handleCaptchaReset();
}
}}
onSuccess={(
value: User | JSONObject,
miscData: JSONObject | undefined,

View File

@@ -4,12 +4,22 @@ import URL from "Common/Types/API/URL";
import Dictionary from "Common/Types/Dictionary";
import { JSONObject } from "Common/Types/JSON";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import ModelForm, { FormType } from "Common/UI/Components/Forms/ModelForm";
import Fields from "Common/UI/Components/Forms/Types/Fields";
import ModelForm, {
FormType,
ModelField,
} from "Common/UI/Components/Forms/ModelForm";
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import Link from "Common/UI/Components/Link/Link";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import { BILLING_ENABLED, DASHBOARD_URL } from "Common/UI/Config";
import Captcha from "Common/UI/Components/Captcha/Captcha";
import {
BILLING_ENABLED,
DASHBOARD_URL,
CAPTCHA_ENABLED,
CAPTCHA_SITE_KEY,
} from "Common/UI/Config";
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
import BaseAPI from "Common/UI/Utils/API/API";
import UiAnalytics from "Common/UI/Utils/Analytics";
@@ -36,6 +46,19 @@ const RegisterPage: () => JSX.Element = () => {
undefined,
);
const isCaptchaEnabled: boolean =
CAPTCHA_ENABLED && Boolean(CAPTCHA_SITE_KEY);
const [shouldResetCaptcha, setShouldResetCaptcha] =
React.useState<boolean>(false);
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
const handleCaptchaReset: () => void = React.useCallback(() => {
setCaptchaResetSignal((current: number) => {
return current + 1;
});
}, []);
if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL);
}
@@ -93,7 +116,7 @@ const RegisterPage: () => JSX.Element = () => {
}
}, []);
let formFields: Fields<User> = [
let formFields: Array<ModelField<User>> = [
{
field: {
email: true,
@@ -183,6 +206,39 @@ const RegisterPage: () => JSX.Element = () => {
},
]);
if (isCaptchaEnabled) {
formFields = formFields.concat([
{
overrideField: {
captchaToken: true,
},
overrideFieldKey: "captchaToken",
fieldType: FormFieldSchemaType.CustomComponent,
title: "Human Verification",
description:
"Complete the captcha challenge so we know you're not a bot.",
required: true,
showEvenIfPermissionDoesNotExist: true,
getCustomElement: (
_values: FormValues<User>,
customProps: CustomElementProps,
) => {
return (
<Captcha
siteKey={CAPTCHA_SITE_KEY}
resetSignal={captchaResetSignal}
error={customProps.error}
onTokenChange={(token: string) => {
customProps.onChange?.(token);
}}
onBlur={customProps.onBlur}
/>
);
},
},
]);
}
if (error) {
return <ErrorMessage message={error} />;
}
@@ -222,7 +278,27 @@ const RegisterPage: () => JSX.Element = () => {
maxPrimaryButtonWidth={true}
fields={formFields}
createOrUpdateApiUrl={apiUrl}
onBeforeCreate={(item: User): Promise<User> => {
onBeforeCreate={(
item: User,
miscDataProps: JSONObject,
): Promise<User> => {
if (isCaptchaEnabled) {
const captchaToken: string | undefined = (
miscDataProps["captchaToken"] as string | undefined
)
?.toString()
.trim();
if (!captchaToken) {
throw new Error(
"Please complete the captcha challenge before signing up.",
);
}
miscDataProps["captchaToken"] = captchaToken;
setShouldResetCaptcha(true);
}
const utmParams: Dictionary<string> = UserUtil.getUtmParams();
if (utmParams && Object.keys(utmParams).length > 0) {
@@ -240,6 +316,16 @@ const RegisterPage: () => JSX.Element = () => {
}}
formType={FormType.Create}
submitButtonText={"Sign Up"}
onLoadingChange={(loading: boolean) => {
if (!isCaptchaEnabled) {
return;
}
if (!loading && shouldResetCaptcha) {
setShouldResetCaptcha(false);
handleCaptchaReset();
}
}}
onSuccess={(value: User, miscData: JSONObject | undefined) => {
if (value && value.email) {
UiAnalytics.userAuth(value.email);

View File

@@ -32,25 +32,25 @@
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@elastic/elasticsearch": "^8.12.1",
"@hcaptcha/react-hcaptcha": "^1.14.0",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-fetch": "^0.52.1",
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
"@opentelemetry/instrumentation": "^0.207.0",
"@opentelemetry/instrumentation-fetch": "^0.207.0",
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-logs": "^0.52.1",
"@opentelemetry/sdk-logs": "^0.207.0",
"@opentelemetry/sdk-metrics": "^1.25.1",
"@opentelemetry/sdk-node": "^0.52.1",
"@opentelemetry/sdk-trace-node": "^1.25.1",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -89,21 +89,21 @@
"node-cron": "^3.0.3",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"pg": "^8.16.3",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.13.0",
"react-big-calendar": "^1.19.4",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.2",
"react-error-boundary": "^4.0.13",
"react-highlight": "^0.15.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.24.1",
"react-router-dom": "^6.30.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^16.0.0",
@@ -118,7 +118,7 @@
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
"stripe": "^10.17.0",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.6.0",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.26",

View File

@@ -3,7 +3,6 @@
<link rel="icon" type="image/png" sizes="194x194" href="/favicon-194x194.png">
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#121212">
<meta name="theme-color" content="#121212">

View File

@@ -1,56 +0,0 @@
{
"name": "OneUptime Account",
"short_name": "OneUptime",
"icons": [
{
"src": "/accounts/assets/img/favicons/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/accounts/assets/img/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": ".",
"theme_color": "#121212",
"background_color": "#121212",
"display": "standalone"
}

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "./assets/img/favicons/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -26,6 +26,7 @@ import {
} from "react-router-dom";
import UserView from "./Pages/Users/View/Index";
import UserDelete from "./Pages/Users/View/Delete";
import UserSettings from "./Pages/Users/View/Settings";
import ProjectView from "./Pages/Projects/View/Index";
import ProjectDelete from "./Pages/Projects/View/Delete";
@@ -71,6 +72,11 @@ const App: () => JSX.Element = () => {
element={<UserView />}
/>
<PageRoute
path={RouteMap[PageMap.USER_SETTINGS]?.toString() || ""}
element={<UserSettings />}
/>
<PageRoute
path={RouteMap[PageMap.USER_DELETE]?.toString() || ""}
element={<UserDelete />}

View File

@@ -0,0 +1,94 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import SideMenuComponent from "./SideMenu";
import User from "Common/Models/DatabaseModels/User";
import ModelPage from "Common/UI/Components/Page/ModelPage";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import FieldType from "Common/UI/Components/Types/FieldType";
const UserSettings: FunctionComponent = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<ModelPage<User>
modelId={modelId}
modelNameField="email"
modelType={User}
title={"User"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Users",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
},
{
title: "User",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_VIEW] as Route,
{
modelId: modelId,
},
),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_SETTINGS] as Route,
{
modelId: modelId,
},
),
},
]}
sideMenu={<SideMenuComponent modelId={modelId} />}
>
<CardModelDetail<User>
name="user-master-admin-settings"
cardProps={{
title: "Master Admin Access",
description:
"Grant or revoke master admin access for this user. Master admins can manage every project and workspace.",
}}
isEditable={true}
editButtonText="Update Access"
formFields={[
{
field: {
isMasterAdmin: true,
},
title: "Master Admin",
description:
"Enable to give this user full access to the entire platform.",
fieldType: FormFieldSchemaType.Toggle,
required: true,
},
]}
modelDetailProps={{
modelType: User,
id: "user-master-admin-settings-detail",
fields: [
{
field: {
isMasterAdmin: true,
},
title: "Master Admin",
fieldType: FieldType.Boolean,
placeholder: "No",
},
],
modelId: modelId,
}}
/>
</ModelPage>
);
};
export default UserSettings;

View File

@@ -30,6 +30,18 @@ const SideMenuComponent: FunctionComponent<SideMenuProps> = (
}}
icon={IconProp.Info}
/>
<SideMenuItem
link={{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_SETTINGS] as Route,
{
modelId: props.modelId,
},
),
}}
icon={IconProp.Settings}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">

View File

@@ -6,6 +6,7 @@ enum PageMap {
USERS = "USERS",
USER_VIEW = "USER_VIEW",
USER_SETTINGS = "USER_SETTINGS",
USER_DELETE = "USER_DELETE",
PROJECTS = "PROJECTS",

View File

@@ -18,6 +18,9 @@ const RouteMap: Dictionary<Route> = {
[PageMap.USERS]: new Route(`/admin/users`),
[PageMap.USER_VIEW]: new Route(`/admin/users/${RouteParams.ModelID}`),
[PageMap.USER_SETTINGS]: new Route(
`/admin/users/${RouteParams.ModelID}/settings`,
),
[PageMap.USER_DELETE]: new Route(
`/admin/users/${RouteParams.ModelID}/delete`,
),

View File

@@ -22,7 +22,6 @@
<!-- End Google Tag Manager -->
<% } %>
<link rel="manifest" href="/admin/assets/img/favicons/ma">
<link rel="apple-touch-icon" sizes="180x180" href="/admin/assets/img/favicons/apple-touch-icon.png">
<link rel="shortcut icon" href="/admin/assets/img/favicons/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/admin/assets/img/favicons/favicon-32x32.png">
@@ -75,20 +74,6 @@
<title>OneUptime Admin Dashboard</title>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
</head>
<body class="h-full bg-gray-50">
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>

View File

@@ -29,12 +29,18 @@ import UserCallAPI from "Common/Server/API/UserCallAPI";
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
import IncidentInternalNoteAPI from "Common/Server/API/IncidentInternalNoteAPI";
import IncidentPublicNoteAPI from "Common/Server/API/IncidentPublicNoteAPI";
import ScheduledMaintenanceInternalNoteAPI from "Common/Server/API/ScheduledMaintenanceInternalNoteAPI";
import ScheduledMaintenancePublicNoteAPI from "Common/Server/API/ScheduledMaintenancePublicNoteAPI";
import IncidentAPI from "Common/Server/API/IncidentAPI";
// User Notification methods.
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
import UserPushAPI from "Common/Server/API/UserPushAPI";
import UserAPI from "Common/Server/API/UserAPI";
import ApiKeyPermissionService, {
Service as ApiKeyPermissionServiceType,
} from "Common/Server/Services/ApiKeyPermissionService";
@@ -60,9 +66,7 @@ import EmailVerificationTokenService, {
import AlertCustomFieldService, {
Service as AlertCustomFieldServiceType,
} from "Common/Server/Services/AlertCustomFieldService";
import AlertInternalNoteService, {
Service as AlertInternalNoteServiceType,
} from "Common/Server/Services/AlertInternalNoteService";
import AlertInternalNoteAPI from "Common/Server/API/AlertInternalNoteAPI";
import AlertNoteTemplateService, {
Service as AlertNoteTemplateServiceType,
} from "Common/Server/Services/AlertNoteTemplateService";
@@ -93,9 +97,6 @@ import AlertStateTimelineService, {
import IncidentCustomFieldService, {
Service as IncidentCustomFieldServiceType,
} from "Common/Server/Services/IncidentCustomFieldService";
import IncidentInternalNoteService, {
Service as IncidentInternalNoteServiceType,
} from "Common/Server/Services/IncidentInternalNoteService";
import IncidentNoteTemplateService, {
Service as IncidentNoteTemplateServiceType,
} from "Common/Server/Services/IncidentNoteTemplateService";
@@ -111,12 +112,6 @@ import IncidentOwnerTeamService, {
import IncidentOwnerUserService, {
Service as IncidentOwnerUserServiceType,
} from "Common/Server/Services/IncidentOwnerUserService";
import IncidentPublicNoteService, {
Service as IncidentPublicNoteServiceType,
} from "Common/Server/Services/IncidentPublicNoteService";
import IncidentService, {
Service as IncidentServiceType,
} from "Common/Server/Services/IncidentService";
import IncidentSeverityService, {
Service as IncidentSeverityServiceType,
} from "Common/Server/Services/IncidentSeverityService";
@@ -232,9 +227,6 @@ import ResellerService, {
import ScheduledMaintenanceCustomFieldService, {
Service as ScheduledMaintenanceCustomFieldServiceType,
} from "Common/Server/Services/ScheduledMaintenanceCustomFieldService";
import ScheduledMaintenanceInternalNoteService, {
Service as ScheduledMaintenanceInternalNoteServiceType,
} from "Common/Server/Services/ScheduledMaintenanceInternalNoteService";
import ScheduledMaintenanceNoteTemplateService, {
Service as ScheduledMaintenanceNoteTemplateServiceType,
} from "Common/Server/Services/ScheduledMaintenanceNoteTemplateService";
@@ -244,9 +236,6 @@ import ScheduledMaintenanceOwnerTeamService, {
import ScheduledMaintenanceOwnerUserService, {
Service as ScheduledMaintenanceOwnerUserServiceType,
} from "Common/Server/Services/ScheduledMaintenanceOwnerUserService";
import ScheduledMaintenancePublicNoteService, {
Service as ScheduledMaintenancePublicNoteServiceType,
} from "Common/Server/Services/ScheduledMaintenancePublicNoteService";
import ScheduledMaintenanceService, {
Service as ScheduledMaintenanceServiceType,
} from "Common/Server/Services/ScheduledMaintenanceService";
@@ -293,9 +282,7 @@ import PushNotificationLogService, {
import SpanService, {
SpanService as SpanServiceType,
} from "Common/Server/Services/SpanService";
import StatusPageAnnouncementService, {
Service as StatusPageAnnouncementServiceType,
} from "Common/Server/Services/StatusPageAnnouncementService";
import StatusPageAnnouncementAPI from "Common/Server/API/StatusPageAnnouncementAPI";
import StatusPageCustomFieldService, {
Service as StatusPageCustomFieldServiceType,
} from "Common/Server/Services/StatusPageCustomFieldService";
@@ -353,9 +340,6 @@ import UserNotificationSettingService, {
import UserOnCallLogService, {
Service as UserNotificationLogServiceType,
} from "Common/Server/Services/UserOnCallLogService";
import UserService, {
Service as UserServiceType,
} from "Common/Server/Services/UserService";
import WorkflowLogService, {
Service as WorkflowLogServiceType,
} from "Common/Server/Services/WorkflowLogService";
@@ -400,7 +384,6 @@ import Dashboard from "Common/Models/DatabaseModels/Dashboard";
import Alert from "Common/Models/DatabaseModels/Alert";
import AlertCustomField from "Common/Models/DatabaseModels/AlertCustomField";
import AlertInternalNote from "Common/Models/DatabaseModels/AlertInternalNote";
import AlertNoteTemplate from "Common/Models/DatabaseModels/AlertNoteTemplate";
import AlertOwnerTeam from "Common/Models/DatabaseModels/AlertOwnerTeam";
import AlertOwnerUser from "Common/Models/DatabaseModels/AlertOwnerUser";
@@ -408,14 +391,11 @@ import AlertSeverity from "Common/Models/DatabaseModels/AlertSeverity";
import AlertState from "Common/Models/DatabaseModels/AlertState";
import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline";
import Incident from "Common/Models/DatabaseModels/Incident";
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
import IncidentInternalNote from "Common/Models/DatabaseModels/IncidentInternalNote";
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote";
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
@@ -450,11 +430,9 @@ import PromoCode from "Common/Models/DatabaseModels/PromoCode";
import Reseller from "Common/Models/DatabaseModels/Reseller";
import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance";
import ScheduledMaintenanceCustomField from "Common/Models/DatabaseModels/ScheduledMaintenanceCustomField";
import ScheduledMaintenanceInternalNote from "Common/Models/DatabaseModels/ScheduledMaintenanceInternalNote";
import ScheduledMaintenanceNoteTemplate from "Common/Models/DatabaseModels/ScheduledMaintenanceNoteTemplate";
import ScheduledMaintenanceOwnerTeam from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam";
import ScheduledMaintenanceOwnerUser from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerUser";
import ScheduledMaintenancePublicNote from "Common/Models/DatabaseModels/ScheduledMaintenancePublicNote";
import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMaintenanceState";
import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
import ServiceCatalog from "Common/Models/DatabaseModels/ServiceCatalog";
@@ -463,7 +441,6 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
// Custom Fields API
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
import StatusPageFooterLink from "Common/Models/DatabaseModels/StatusPageFooterLink";
@@ -482,7 +459,6 @@ import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
import User from "Common/Models/DatabaseModels/User";
import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationRule";
import UserNotificationSetting from "Common/Models/DatabaseModels/UserNotificationSetting";
import UserOnCallLog from "Common/Models/DatabaseModels/UserOnCallLog";
@@ -848,10 +824,7 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<AlertInternalNote, AlertInternalNoteServiceType>(
AlertInternalNote,
AlertInternalNoteService,
).getRouter(),
new AlertInternalNoteAPI().getRouter(),
);
app.use(
@@ -1048,10 +1021,7 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<StatusPageAnnouncement, StatusPageAnnouncementServiceType>(
StatusPageAnnouncement,
StatusPageAnnouncementService,
).getRouter(),
new StatusPageAnnouncementAPI().getRouter(),
);
app.use(
@@ -1295,13 +1265,7 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<Incident, IncidentServiceType>(
Incident,
IncidentService,
).getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new IncidentAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
@@ -1724,40 +1688,22 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
ScheduledMaintenancePublicNote,
ScheduledMaintenancePublicNoteServiceType
>(
ScheduledMaintenancePublicNote,
ScheduledMaintenancePublicNoteService,
).getRouter(),
new ScheduledMaintenancePublicNoteAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
ScheduledMaintenanceInternalNote,
ScheduledMaintenanceInternalNoteServiceType
>(
ScheduledMaintenanceInternalNote,
ScheduledMaintenanceInternalNoteService,
).getRouter(),
new ScheduledMaintenanceInternalNoteAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentPublicNote, IncidentPublicNoteServiceType>(
IncidentPublicNote,
IncidentPublicNoteService,
).getRouter(),
new IncidentPublicNoteAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentInternalNote, IncidentInternalNoteServiceType>(
IncidentInternalNote,
IncidentInternalNoteService,
).getRouter(),
new IncidentInternalNoteAPI().getRouter(),
);
app.use(
@@ -1874,10 +1820,7 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(`/${APP_NAME.toLocaleLowerCase()}`, TelemetryAPI);
//attach api's
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<User, UserServiceType>(User, UserService).getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserAPI().getRouter());
},
};

View File

@@ -25,24 +25,71 @@ import EmailVerificationTokenService from "Common/Server/Services/EmailVerificat
import MailService from "Common/Server/Services/MailService";
import UserService from "Common/Server/Services/UserService";
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
import UserSessionService, {
SessionMetadata,
} from "Common/Server/Services/UserSessionService";
import CookieUtil from "Common/Server/Utils/Cookie";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import CaptchaUtil from "Common/Server/Utils/Captcha";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import TotpAuth from "Common/Server/Utils/TotpAuth";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
import User from "Common/Models/DatabaseModels/User";
import UserSession from "Common/Models/DatabaseModels/UserSession";
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
type FinalizeUserLoginInput = {
req: ExpressRequest;
res: ExpressResponse;
user: User;
isGlobalLogin: boolean;
};
const finalizeUserLogin: (
data: FinalizeUserLoginInput,
) => Promise<SessionMetadata> = async (
data: FinalizeUserLoginInput,
): Promise<SessionMetadata> => {
const { req, res, user, isGlobalLogin } = data;
const sessionMetadata: SessionMetadata =
await UserSessionService.createSession({
userId: user.id!,
isGlobalLogin,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
CookieUtil.setUserCookie({
expressResponse: res,
user,
isGlobalLogin,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return sessionMetadata;
};
router.post(
"/signup",
async (
@@ -61,6 +108,16 @@ router.post(
);
}
const miscDataProps: JSONObject =
(req.body["miscDataProps"] as JSONObject) || {};
await CaptchaUtil.verifyCaptcha({
token:
(miscDataProps["captchaToken"] as string | undefined) ||
(req.body["captchaToken"] as string | undefined),
remoteIp: getClientIp(req) || null,
});
const data: JSONObject = req.body["data"];
/* Creating a type that is a partial of the TBaseModel type. */
@@ -185,9 +242,9 @@ router.post(
if (savedUser) {
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(savedUser.id!);
CookieUtil.setUserCookie({
expressResponse: res,
await finalizeUserLogin({
req,
res,
user: savedUser,
isGlobalLogin: true,
});
@@ -487,6 +544,127 @@ router.post(
},
);
router.post(
"/refresh-token",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
if (!refreshToken) {
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException(
"Refresh token missing. Please login again.",
),
);
}
const session: UserSession | null =
await UserSessionService.findActiveSessionByRefreshToken(refreshToken);
if (!session || !session.id) {
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (
session.refreshTokenExpiresAt &&
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
) {
await UserSessionService.revokeSessionById(session.id, {
reason: "Refresh token expired",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (!session.userId) {
await UserSessionService.revokeSessionById(session.id, {
reason: "Session missing user",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
const user: User | null = await UserService.findOneById({
id: session.userId,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
name: true,
isMasterAdmin: true,
profilePictureId: true,
timezone: true,
enableTwoFactorAuth: true,
},
});
if (!user) {
await UserSessionService.revokeSessionById(session.id, {
reason: "User not found",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Account no longer exists."),
);
}
const additionalInfo: JSONObject = (session.additionalInfo ||
{}) as JSONObject;
const isGlobalLogin: boolean =
typeof additionalInfo["isGlobalLogin"] === "boolean"
? (additionalInfo["isGlobalLogin"] as boolean)
: true;
const renewedSession: SessionMetadata =
await UserSessionService.renewSessionWithNewRefreshToken({
session,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
CookieUtil.setUserCookie({
expressResponse: res,
user,
isGlobalLogin,
sessionId: renewedSession.session.id!,
refreshToken: renewedSession.refreshToken,
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/logout",
async (
@@ -495,6 +673,15 @@ router.post(
next: NextFunction,
): Promise<void> => {
try {
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
if (refreshToken) {
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
reason: "User logout",
});
}
CookieUtil.removeAllCookies(req, res);
return Response.sendEmptySuccessResponse(req, res);
@@ -628,6 +815,18 @@ const login: LoginFunction = async (options: {
const verifyWebAuthn: boolean = options.verifyWebAuthn;
try {
const miscDataProps: JSONObject =
(req.body["miscDataProps"] as JSONObject) || {};
if (!verifyTotpAuth && !verifyWebAuthn) {
await CaptchaUtil.verifyCaptcha({
token:
(miscDataProps["captchaToken"] as string | undefined) ||
(req.body["captchaToken"] as string | undefined),
remoteIp: getClientIp(req) || null,
});
}
const data: JSONObject = req.body["data"];
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
@@ -788,8 +987,9 @@ const login: LoginFunction = async (options: {
if (alreadySavedUser.password.toString() === user.password!.toString()) {
logger.info("User logged in: " + alreadySavedUser.email?.toString());
CookieUtil.setUserCookie({
expressResponse: res,
await finalizeUserLogin({
req,
res,
user: alreadySavedUser,
isGlobalLogin: true,
});

View File

@@ -470,174 +470,181 @@ router.get(
},
);
// Update User - PUT /scim/v2/Users/{id}
router.put(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
const handleUserUpdate: (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
) => Promise<void> = async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
logger.debug(
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
);
logger.debug(
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
);
logger.debug(
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
logger.debug(
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and is part of the project
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
query: {
projectId: projectId,
userId: new ObjectID(userId),
},
select: {
userId: true,
user: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
},
props: { isRoot: true },
});
if (!projectUser || !projectUser.user) {
logger.debug(
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
);
throw new NotFoundException("User not found or not part of this project");
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const name: string = parseNameFromSCIM(scimUser);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
);
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
// Handle user deactivation by removing from teams
if (active === false && !scimConfig.enablePushGroups) {
logger.debug(
`SCIM Update user - user marked as inactive, removing from teams`,
);
await handleUserTeamOperations(
"remove",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully removed from teams due to deactivation`,
);
}
// Handle user activation by adding to teams
if (active === true && !scimConfig.enablePushGroups) {
logger.debug(`SCIM Update user - user marked as active, adding to teams`);
await handleUserTeamOperations(
"add",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully added to teams due to activation`,
);
}
if (email || name) {
const updateData: any = {};
if (email) {
updateData.email = new Email(email);
}
if (name) {
updateData.name = new Name(name);
}
// Check if user exists and is part of the project
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
query: {
projectId: projectId,
userId: new ObjectID(userId),
},
logger.debug(
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await UserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(`SCIM Update user - user updated successfully`);
// Fetch updated user
const updatedUser: User | null = await UserService.findOneById({
id: new ObjectID(userId),
select: {
userId: true,
user: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (!projectUser || !projectUser.user) {
logger.debug(
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this project",
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const name: string = parseNameFromSCIM(scimUser);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
);
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
// Handle user deactivation by removing from teams
if (active === false && !scimConfig.enablePushGroups) {
logger.debug(
`SCIM Update user - user marked as inactive, removing from teams`,
);
await handleUserTeamOperations(
"remove",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully removed from teams due to deactivation`,
);
}
// Handle user activation by adding to teams
if (active === true && !scimConfig.enablePushGroups) {
logger.debug(
`SCIM Update user - user marked as active, adding to teams`,
);
await handleUserTeamOperations(
"add",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully added to teams due to activation`,
);
}
if (email || name) {
const updateData: any = {};
if (email) {
updateData.email = new Email(email);
}
if (name) {
updateData.name = new Name(name);
}
logger.debug(
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await UserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(`SCIM Update user - user updated successfully`);
// Fetch updated user
const updatedUser: User | null = await UserService.findOneById({
id: new ObjectID(userId),
select: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
}
}
logger.debug(
`SCIM Update user - no updates made, returning existing user`,
);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
projectUser.user,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return next(err);
}
},
logger.debug(`SCIM Update user - no updates made, returning existing user`);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
projectUser.user,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return next(err);
}
};
// Update User - PUT /scim/v2/Users/{id}
router.put(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
handleUserUpdate,
);
// Update User - PATCH /scim/v2/Users/{id}
router.patch(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
handleUserUpdate,
);
// Groups endpoint - GET /scim/v2/Groups

View File

@@ -20,6 +20,9 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
import ProjectSSOService from "Common/Server/Services/ProjectSsoService";
import TeamMemberService from "Common/Server/Services/TeamMemberService";
import UserService from "Common/Server/Services/UserService";
import UserSessionService, {
SessionMetadata,
} from "Common/Server/Services/UserSessionService";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
import Select from "Common/Server/Types/Database/Select";
import CookieUtil from "Common/Server/Utils/Cookie";
@@ -28,6 +31,9 @@ import Express, {
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -40,6 +46,8 @@ import Name from "Common/Types/Name";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
/*
* This route is used to get the SSO config for the user.
* when the user logs in from OneUptime and not from the IDP.
@@ -539,15 +547,31 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
expressResponse: res,
});
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
const sessionMetadata: SessionMetadata =
await UserSessionService.createSession({
userId: alreadySavedUser.id!,
isGlobalLogin: false,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
additionalInfo: {
projectId: projectId.toString(),
},
});
CookieUtil.setUserCookie({
expressResponse: res,
user: alreadySavedUser,
isGlobalLogin: false,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

View File

@@ -1,35 +1,145 @@
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import { FileRoute } from "Common/ServiceRoute";
import { StatusPageApiRoute } from "Common/ServiceRoute";
import Hostname from "Common/Types/API/Hostname";
import Protocol from "Common/Types/API/Protocol";
import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import BadDataException from "Common/Types/Exception/BadDataException";
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from "Common/Server/DatabaseConfig";
import { EncryptionSecret } from "Common/Server/EnvironmentConfig";
import MailService from "Common/Server/Services/MailService";
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
import StatusPageService from "Common/Server/Services/StatusPageService";
import StatusPagePrivateUserSessionService, {
SessionMetadata as StatusPageSessionMetadata,
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
import CookieUtil from "Common/Server/Utils/Cookie";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
import StatusPagePrivateUserSession from "Common/Models/DatabaseModels/StatusPagePrivateUserSession";
import { MASTER_PASSWORD_COOKIE_IDENTIFIER } from "Common/Types/StatusPage/MasterPassword";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
type MasterPasswordAuthInput = {
req: ExpressRequest;
res?: ExpressResponse;
statusPageId: ObjectID;
};
const hasValidMasterPasswordSession: (
data: MasterPasswordAuthInput,
) => boolean = (data: MasterPasswordAuthInput): boolean => {
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
data.req,
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
);
if (!token) {
return false;
}
try {
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
return (
payload["statusPageId"] === data.statusPageId.toString() &&
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
);
} catch (err) {
logger.error(err);
}
return false;
};
const respondWithMasterPasswordAccess: (
data: MasterPasswordAuthInput & { res: ExpressResponse },
) => boolean = (
data: MasterPasswordAuthInput & { res: ExpressResponse },
): boolean => {
if (!hasValidMasterPasswordSession(data)) {
return false;
}
Response.sendEmptySuccessResponse(data.req, data.res);
return true;
};
type FinalizeStatusPageLoginInput = {
req: ExpressRequest;
res: ExpressResponse;
user: StatusPagePrivateUser;
};
const finalizeStatusPageLogin: (data: FinalizeStatusPageLoginInput) => Promise<{
sessionMetadata: StatusPageSessionMetadata;
accessToken: string;
}> = async (
data: FinalizeStatusPageLoginInput,
): Promise<{
sessionMetadata: StatusPageSessionMetadata;
accessToken: string;
}> => {
const { req, res, user } = data;
if (!user.projectId) {
throw new BadDataException(
"Status page user is missing associated projectId.",
);
}
if (!user.statusPageId) {
throw new BadDataException(
"Status page user is missing associated statusPageId.",
);
}
const sessionMetadata: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.createSession({
projectId: user.projectId,
statusPageId: user.statusPageId,
statusPagePrivateUserId: user.id!,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user,
statusPageId: user.statusPageId,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return {
sessionMetadata,
accessToken,
};
};
router.post(
"/logout/:statuspageid",
async (
@@ -46,7 +156,21 @@ router.post(
req.params["statuspageid"].toString(),
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId)); // remove the cookie.
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
if (refreshToken) {
await StatusPagePrivateUserSessionService.revokeSessionByRefreshToken(
refreshToken,
{
reason: "User logged out",
},
);
}
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(res, CookieUtil.getRefreshTokenKey(statusPageId));
CookieUtil.removeStatusPageMasterPasswordCookie(res, statusPageId);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
@@ -55,6 +179,258 @@ router.post(
},
);
router.post(
"/refresh-token/:statuspageid",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const statusPageIdParam: string | undefined = req.params["statuspageid"];
if (!statusPageIdParam) {
throw new BadDataException("Status Page ID is required.");
}
const statusPageId: ObjectID = new ObjectID(statusPageIdParam.toString());
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
if (!refreshToken) {
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException(
"Refresh token missing. Please login again.",
),
);
}
const session: StatusPagePrivateUserSession | null =
await StatusPagePrivateUserSessionService.findActiveSessionByRefreshToken(
refreshToken,
);
if (!session || !session.id || !session.statusPageId) {
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (session.statusPageId.toString() !== statusPageId.toString()) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Status page mismatch",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (
session.refreshTokenExpiresAt &&
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Refresh token expired",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (!session.statusPagePrivateUserId) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Session missing user",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
const user: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: session.statusPagePrivateUserId,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
});
if (!user) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "User not found",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
if (
respondWithMasterPasswordAccess({
req,
res,
statusPageId,
})
) {
return;
}
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Account no longer exists."),
);
}
const renewedSession: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.renewSessionWithNewRefreshToken(
{
session,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
},
);
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user,
statusPageId: user.statusPageId!,
sessionId: renewedSession.session.id!,
refreshToken: renewedSession.refreshToken,
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendEntityResponse(
req,
res,
user,
StatusPagePrivateUser,
{
miscData: {
token: accessToken,
},
},
);
} catch (err) {
return next(err);
}
},
);
router.post(
"/forgot-password",
async (
@@ -146,6 +522,8 @@ router.post(
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const statusPageIdString: string | null =
statusPage.id?.toString() || statusPage._id?.toString() || null;
MailService.sendMail(
{
@@ -154,12 +532,13 @@ router.post(
templateType: EmailTemplateType.StatusPageForgotPassword,
vars: {
statusPageName: statusPageName!,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
logoUrl:
statusPage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
homeURL: statusPageURL,
tokenVerifyUrl: URL.fromString(statusPageURL)
.addRoute("/reset-password/" + token)
@@ -288,6 +667,8 @@ router.post(
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const statusPageIdString: string | null =
statusPage.id?.toString() || statusPage._id?.toString() || null;
MailService.sendMail(
{
@@ -297,12 +678,13 @@ router.post(
vars: {
homeURL: statusPageURL,
statusPageName: statusPageName || "",
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
logoUrl:
statusPage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
},
},
{
@@ -376,6 +758,7 @@ router.post(
password: true,
email: true,
statusPageId: true,
projectId: true,
},
props: {
isRoot: true,
@@ -383,31 +766,38 @@ router.post(
});
if (alreadySavedUser) {
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30),
),
const { accessToken } = await finalizeStatusPageLogin({
req,
res,
user: alreadySavedUser,
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
},
);
const sanitizedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: alreadySavedUser.id!,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
});
if (!sanitizedUser && (alreadySavedUser as any).password) {
delete (alreadySavedUser as any).password;
}
return Response.sendEntityResponse(
req,
res,
alreadySavedUser,
sanitizedUser || alreadySavedUser,
StatusPagePrivateUser,
{
miscData: {
token: token,
token: accessToken,
},
},
);

View File

@@ -355,45 +355,124 @@ router.post(
},
);
// Update Status Page User - PUT /status-page-scim/v2/Users/{id}
router.put(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
const handleStatusPageUserUpdate: (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
) => Promise<void> = async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
logger.debug(
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
);
logger.debug(
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and belongs to this status page
const statusPageUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
_id: new ObjectID(userId),
},
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (!statusPageUser) {
logger.debug(
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
`Status Page SCIM Update user - user not found for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this status page",
);
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
);
// Handle user deactivation by deleting from status page
if (active === false) {
logger.debug(
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
);
logger.debug(
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
const scimConfig: StatusPageSCIM = bearerData[
"scimConfig"
] as StatusPageSCIM;
if (scimConfig.autoDeprovisionUsers) {
await StatusPagePrivateUserService.deleteOneById({
id: new ObjectID(userId),
props: { isRoot: true },
});
if (!userId) {
throw new BadRequestException("User ID is required");
logger.debug(
`Status Page SCIM Update user - user removed from status page`,
);
// Return empty response for deleted user
return Response.sendJsonObjectResponse(req, res, {});
}
}
// Check if user exists and belongs to this status page
const statusPageUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
_id: new ObjectID(userId),
},
// Prepare update data
const updateData: {
email?: Email;
} = {};
if (email && email !== statusPageUser.email?.toString()) {
updateData.email = new Email(email);
}
// Only update if there are changes
if (Object.keys(updateData).length > 0) {
logger.debug(
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await StatusPagePrivateUserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(`Status Page SCIM Update user - user updated successfully`);
// Fetch updated user
const updatedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: new ObjectID(userId),
select: {
_id: true,
email: true,
@@ -403,116 +482,48 @@ router.put(
props: { isRoot: true },
});
if (!statusPageUser) {
logger.debug(
`Status Page SCIM Update user - user not found for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this status page",
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
);
// Handle user deactivation by deleting from status page
if (active === false) {
logger.debug(
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
);
const scimConfig: StatusPageSCIM = bearerData[
"scimConfig"
] as StatusPageSCIM;
if (scimConfig.autoDeprovisionUsers) {
await StatusPagePrivateUserService.deleteOneById({
id: new ObjectID(userId),
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Update user - user removed from status page`,
);
// Return empty response for deleted user
return Response.sendJsonObjectResponse(req, res, {});
}
}
// Prepare update data
const updateData: {
email?: Email;
} = {};
if (email && email !== statusPageUser.email?.toString()) {
updateData.email = new Email(email);
}
// Only update if there are changes
if (Object.keys(updateData).length > 0) {
logger.debug(
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await StatusPagePrivateUserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Update user - user updated successfully`,
);
// Fetch updated user
const updatedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: new ObjectID(userId),
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
}
}
logger.debug(
`Status Page SCIM Update user - no updates made, returning existing user`,
);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
statusPageUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return next(err);
}
},
logger.debug(
`Status Page SCIM Update user - no updates made, returning existing user`,
);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
statusPageUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return next(err);
}
};
// Update Status Page User - PUT /status-page-scim/v2/Users/{id}
router.put(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
handleStatusPageUserUpdate,
);
// Update Status Page User - PATCH /status-page-scim/v2/Users/{id}
router.patch(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
handleStatusPageUserUpdate,
);
// Delete Status Page User - DELETE /status-page-scim/v2/Users/{id}

View File

@@ -1,6 +1,5 @@
import SSOUtil from "../Utils/SSO";
import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
import Email from "Common/Types/Email";
import BadRequestException from "Common/Types/Exception/BadRequestException";
import Exception from "Common/Types/Exception/Exception";
@@ -8,9 +7,11 @@ import ServerException from "Common/Types/Exception/ServerException";
import HashedString from "Common/Types/HashedString";
import { JSONObject } from "Common/Types/JSON";
import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
import StatusPagePrivateUserSessionService, {
SessionMetadata as StatusPageSessionMetadata,
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
import StatusPageService from "Common/Server/Services/StatusPageService";
import StatusPageSsoService from "Common/Server/Services/StatusPageSsoService";
import CookieUtil from "Common/Server/Utils/Cookie";
@@ -19,8 +20,10 @@ import Express, {
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
@@ -30,6 +33,8 @@ import xml2js from "xml2js";
// Initialize Express router.
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
// Define a GET route for SSO in a status page context.
router.get(
"/status-page-sso/:statusPageId/:statusPageSsoId",
@@ -285,24 +290,30 @@ router.post(
});
}
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30),
),
if (!alreadySavedUser.projectId) {
alreadySavedUser.projectId = projectId;
}
const sessionMetadata: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.createSession({
projectId: alreadySavedUser.projectId!,
statusPageId: statusPageId,
statusPagePrivateUserId: alreadySavedUser.id!,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
const token: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user: alreadySavedUser,
statusPageId: statusPageId,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
},
);
// get status page URL.
const statusPageURL: string =
await StatusPageService.getStatusPageFirstURL(statusPageId);

View File

@@ -42,8 +42,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

View File

@@ -13,8 +13,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}

View File

@@ -20,8 +20,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}

View File

@@ -17,8 +17,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

View File

@@ -0,0 +1,35 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Postmortem Published: " incidentTitle) }}
{{> InfoBlock info="A postmortem has been published for an incident. Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=incidentTitle text="" }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Postmortem: " text="" }}
{{> DetailBoxField title="" text=postmortemNote }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{> End this}}

View File

@@ -14,8 +14,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

View File

@@ -24,8 +24,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

View File

@@ -25,8 +25,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

View File

@@ -20,8 +20,11 @@
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}

37
App/package-lock.json generated
View File

@@ -39,25 +39,25 @@
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@elastic/elasticsearch": "^8.12.1",
"@hcaptcha/react-hcaptcha": "^1.14.0",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-fetch": "^0.52.1",
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
"@opentelemetry/instrumentation": "^0.207.0",
"@opentelemetry/instrumentation-fetch": "^0.207.0",
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-logs": "^0.52.1",
"@opentelemetry/sdk-logs": "^0.207.0",
"@opentelemetry/sdk-metrics": "^1.25.1",
"@opentelemetry/sdk-node": "^0.52.1",
"@opentelemetry/sdk-trace-node": "^1.25.1",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
@@ -96,21 +96,21 @@
"node-cron": "^3.0.3",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"pg": "^8.16.3",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.13.0",
"react-big-calendar": "^1.19.4",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.2",
"react-error-boundary": "^4.0.13",
"react-highlight": "^0.15.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.24.1",
"react-router-dom": "^6.30.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^16.0.0",
@@ -125,7 +125,7 @@
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
"stripe": "^10.17.0",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.6.0",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.26",
@@ -3501,10 +3501,11 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"

View File

@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
@@ -418,6 +419,7 @@ export default class AlertFeed extends BaseModel {
],
update: [],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
required: true,

View File

@@ -1,6 +1,7 @@
import Alert from "./Alert";
import Project from "./Project";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("alert")
@@ -340,6 +349,54 @@ export default class AlertInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlertInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditAlertInternalNote,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "AlertInternalNoteFile",
joinColumn: {
name: "alertInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -378,6 +379,7 @@ export default class AlertSeverity extends BaseModel {
Permission.EditAlertSeverity,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -356,6 +357,7 @@ export default class AlertState extends BaseModel {
Permission.EditAlertState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -20,7 +20,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@AllowAccessIfSubscriptionIsUnpaid()
@TenantColumn("projectId")
@TableAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectUser,
@@ -28,7 +32,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectMember,
Permission.ReadBillingPaymentMethod,
],
delete: [Permission.ProjectOwner, Permission.DeleteBillingPaymentMethod],
delete: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.DeleteBillingPaymentMethod,
],
update: [],
})
@CrudApiEndpoint(new Route("/billing-payment-methods"))
@@ -45,7 +53,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
})
export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectUser,
@@ -77,7 +89,11 @@ export default class BillingPaymentMethod extends BaseModel {
public project?: Project = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -103,7 +119,11 @@ export default class BillingPaymentMethod extends BaseModel {
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -136,7 +156,11 @@ export default class BillingPaymentMethod extends BaseModel {
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -218,7 +242,11 @@ export default class BillingPaymentMethod extends BaseModel {
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -278,7 +306,11 @@ export default class BillingPaymentMethod extends BaseModel {
public paymentProviderCustomerId?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -298,7 +330,11 @@ export default class BillingPaymentMethod extends BaseModel {
public last4Digits?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
create: [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,

View File

@@ -15,6 +15,7 @@ import TableColumn, {
getTableColumns,
} from "../../../Types/Database/TableColumn";
import TableColumnType from "../../../Types/Database/TableColumnType";
import { getFirstColorFieldColumn } from "../../../Types/Database/ColorField";
import Dictionary from "../../../Types/Dictionary";
import Email from "../../../Types/Email";
import BadDataException from "../../../Types/Exception/BadDataException";
@@ -57,6 +58,7 @@ export default class DatabaseBaseModel extends BaseEntity {
type: TableColumnType.ObjectID,
description: "ID of this object",
computed: true,
canReadOnRelationQuery: true,
})
@PrimaryGeneratedColumn("uuid")
public _id?: string = undefined;
@@ -203,6 +205,10 @@ export default class DatabaseBaseModel extends BaseEntity {
return new Columns(Object.keys(getTableColumns(this)));
}
public getFirstColorColumn(): string | null {
return getFirstColorFieldColumn(this);
}
public canQueryMultiTenant(): boolean {
return Boolean(this.isMultiTenantRequestAllowed);
}

View File

@@ -22,7 +22,7 @@ import { Entity } from "typeorm";
@CrudApiEndpoint(new Route("/file"))
@TableAccessControl({
create: [Permission.CurrentUser, Permission.AuthenticatedRequest],
read: [Permission.CurrentUser, Permission.AuthenticatedRequest],
read: [],
delete: [],
update: [],
})

View File

@@ -7,6 +7,7 @@ import OnCallDutyPolicy from "./OnCallDutyPolicy";
import Probe from "./Probe";
import Project from "./Project";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -228,6 +229,43 @@ export default class Incident extends BaseModel {
})
public description?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.Date,
title: "Declared At",
description: "Date and time when this incident was declared.",
isDefaultValueColumn: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
default: () => {
return "now()";
},
})
public declaredAt?: Date = undefined;
@Index()
@ColumnAccessControl({
create: [],
@@ -803,6 +841,77 @@ export default class Incident extends BaseModel {
})
public subscriberNotificationStatusMessage?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
isDefaultValueColumn: true,
computed: true,
hideColumnInDocumentation: true,
type: TableColumnType.ShortText,
title: "Subscriber Notification Status on Postmortem Published",
description:
"Status of notification sent to subscribers about this incident postmortem",
defaultValue: StatusPageSubscriberNotificationStatus.Pending,
})
@Column({
type: ColumnType.ShortText,
default: StatusPageSubscriberNotificationStatus.Pending,
})
public subscriberNotificationStatusOnPostmortemPublished?: StatusPageSubscriberNotificationStatus =
undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Notification Status Message on Postmortem Published",
description:
"Status message for subscriber notifications on postmortem published - includes success messages, failure reasons, or skip reasons",
required: false,
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
})
public subscriberNotificationStatusMessageOnPostmortemPublished?: string =
undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@@ -959,6 +1068,151 @@ export default class Incident extends BaseModel {
})
public postmortemNote?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Show postmortem on status page?",
description:
"Should the postmortem note and attachments be visible on the status page once published?",
defaultValue: false,
isDefaultValueColumn: true,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public showPostmortemOnStatusPage?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Notify Subscribers on Postmortem Published",
description:
"Should subscribers be notified when the postmortem is published?",
defaultValue: true,
isDefaultValueColumn: true,
})
@Column({
type: ColumnType.Boolean,
default: true,
})
public notifySubscribersOnPostmortemPublished?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.Date,
title: "Postmortem Posted At",
description:
"Timestamp that will be shown alongside the published postmortem on the status page.",
required: false,
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public postmortemPostedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Postmortem Attachments",
description:
"Files that accompany the postmortem note and can be shared publicly when enabled.",
required: false,
})
@ManyToMany(() => {
return File;
})
@JoinTable({
name: "IncidentPostmortemAttachmentFile",
joinColumn: {
name: "incidentId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public postmortemAttachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -1,6 +1,7 @@
import Incident from "./Incident";
import Project from "./Project";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("incident")
@@ -340,6 +349,54 @@ export default class IncidentInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentInternalNote,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "IncidentInternalNoteFile",
joinColumn: {
name: "incidentInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -1,6 +1,7 @@
import Incident from "./Incident";
import Project from "./Project";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("incident")
@@ -341,6 +350,54 @@ export default class IncidentPublicNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentPublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentPublicNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentPublicNote,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "IncidentPublicNoteFile",
joinColumn: {
name: "incidentPublicNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -378,6 +379,7 @@ export default class IncidentSeverity extends BaseModel {
Permission.EditIncidentSeverity,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -380,6 +381,7 @@ export default class IncidentState extends BaseModel {
Permission.EditIncidentState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -118,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
import StatusPageOwnerUser from "./StatusPageOwnerUser";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
import StatusPageResource from "./StatusPageResource";
import StatusPageSCIM from "./StatusPageSCIM";
import StatusPageSSO from "./StatusPageSso";
@@ -130,6 +131,7 @@ import TeamComplianceSetting from "./TeamComplianceSetting";
import TelemetryService from "./TelemetryService";
import UsageBilling from "./TelemetryUsageBilling";
import User from "./User";
import UserSession from "./UserSession";
import UserCall from "./UserCall";
// Notification Methods
import UserEmail from "./UserEmail";
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
StatusPageFooterLink,
StatusPageHeaderLink,
StatusPagePrivateUser,
StatusPagePrivateUserSession,
StatusPageHistoryChartBarColorRule,
ScheduledMaintenanceState,
@@ -375,6 +378,7 @@ const AllModelTypes: Array<{
ProbeOwnerTeam,
ProbeOwnerUser,
UserSession,
UserTotpAuth,
UserWebAuthn,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -365,6 +366,7 @@ export default class Label extends AccessControlModel {
Permission.EditProjectLabel,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -379,6 +380,7 @@ export default class MonitorStatus extends BaseModel {
Permission.EditProjectMonitorStatus,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
@@ -422,6 +423,7 @@ export default class ScheduledMaintenanceFeed extends BaseModel {
],
update: [],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
required: true,

View File

@@ -1,6 +1,7 @@
import Project from "./Project";
import ScheduledMaintenance from "./ScheduledMaintenance";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -16,7 +17,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@CanAccessIfCanReadOn("scheduledMaintenance")
@TenantColumn("projectId")
@@ -340,6 +349,54 @@ export default class ScheduledMaintenanceInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenanceInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenanceInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditScheduledMaintenanceInternalNote,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "ScheduledMaintenanceInternalNoteFile",
joinColumn: {
name: "scheduledMaintenanceInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -1,6 +1,7 @@
import Project from "./Project";
import ScheduledMaintenance from "./ScheduledMaintenance";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("scheduledMaintenance")
@@ -342,6 +351,54 @@ export default class ScheduledMaintenancePublicNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenancePublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenancePublicNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditScheduledMaintenancePublicNote,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "ScheduledMaintenancePublicNoteFile",
joinColumn: {
name: "scheduledMaintenancePublicNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -380,6 +381,7 @@ export default class ScheduledMaintenanceState extends BaseModel {
Permission.EditScheduledMaintenanceState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -14,6 +14,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -448,6 +449,7 @@ export default class ServiceCatalog extends BaseModel {
Permission.EditServiceCatalog,
],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
title: "Service Color",

View File

@@ -30,6 +30,7 @@ import { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import Timezone from "../../Types/Timezone";
import HashedString from "../../Types/HashedString";
import {
Column,
Entity,
@@ -883,6 +884,78 @@ export default class StatusPage extends BaseModel {
})
public isPublicStatusPage?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable Master Password",
description:
"Require visitors to enter a master password before viewing a private status page.",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public enableMasterPassword?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
// This is a hashed column. So, reading the value is does not affect anything.
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@TableColumn({
title: "Master Password",
description:
"Password required to unlock a private status page. This value is stored as a secure hash.",
hashed: true,
type: TableColumnType.HashedString,
placeholder: "Enter a new master password",
})
@Column({
type: ColumnType.HashedString,
length: ColumnLength.HashedString,
nullable: true,
transformer: HashedString.getDatabaseTransformer(),
})
public masterPassword?: HashedString = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -2,6 +2,7 @@ import Monitor from "./Monitor";
import Project from "./Project";
import StatusPage from "./StatusPage";
import User from "./User";
import File from "./File";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
@@ -375,6 +376,54 @@ export default class StatusPageAnnouncement extends BaseModel {
})
public description?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageAnnouncement,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageAnnouncement,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageAnnouncement,
],
})
@TableColumn({
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this announcement",
required: false,
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "StatusPageAnnouncementFile",
joinColumn: {
name: "statusPageAnnouncementId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -282,8 +282,9 @@ export default class StatusPageDomain extends BaseModel {
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Sumdomain",
description: "Subdomain of your status page - like (status)",
title: "Subdomain",
description:
"Subdomain label for your status page such as 'status'. Leave blank or enter @ to use the root domain.",
})
@Column({
nullable: false,

View File

@@ -0,0 +1,413 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Project from "./Project";
import StatusPage from "./StatusPage";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import HashedString from "../../Types/HashedString";
import IconProp from "../../Types/Icon/IconProp";
import { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@AllowAccessIfSubscriptionIsUnpaid()
@TableBillingAccessControl({
create: PlanType.Growth,
read: PlanType.Growth,
update: PlanType.Growth,
delete: PlanType.Growth,
})
@CanAccessIfCanReadOn("statusPage")
@TenantColumn("projectId")
@TableAccessControl({
create: [],
read: [],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/status-page-private-user-session"))
@Entity({
name: "StatusPagePrivateUserSession",
})
@TableMetadata({
tableName: "StatusPagePrivateUserSession",
singularName: "Status Page Private User Session",
pluralName: "Status Page Private User Sessions",
icon: IconProp.Lock,
tableDescription:
"Stores status page private user sessions, refresh tokens, and device metadata for secure access control.",
})
export default class StatusPagePrivateUserSession extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Project that owns this private status page session.",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Project ID",
description: "Project identifier for this session.",
required: true,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status page associated with this session.",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Status Page ID",
description: "Identifier for the status page.",
required: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPagePrivateUserId",
type: TableColumnType.Entity,
modelType: StatusPagePrivateUser,
title: "Status Page Private User",
description: "Private user record associated with this session.",
})
@ManyToOne(
() => {
return StatusPagePrivateUser;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "delete",
},
)
@JoinColumn({ name: "statusPagePrivateUserId" })
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Status Page Private User ID",
description: "Identifier for the status page private user.",
required: true,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPagePrivateUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index({ unique: true })
@TableColumn({
type: TableColumnType.HashedString,
title: "Refresh Token",
description: "Hashed refresh token for the private user session.",
required: true,
hideColumnInDocumentation: true,
})
@Column({
type: ColumnType.HashedString,
length: ColumnLength.HashedString,
nullable: false,
unique: true,
transformer: HashedString.getDatabaseTransformer(),
})
public refreshToken?: HashedString = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Refresh Token Expires At",
description: "Expiration timestamp for the refresh token.",
required: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Active At",
description: "Last time this session was active.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public lastActiveAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Name",
description: "Friendly name for the device used to access the status page.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Type",
description: "Type of device (desktop, mobile, etc).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceType?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device OS",
description: "Operating system reported for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceOS?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Browser",
description: "Browser or client application used for the session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceBrowser?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address recorded for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "User Agent",
description: "User agent string supplied by the client.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Indicates if the session has been revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Revoked At",
description: "Timestamp when the session was revoked, if applicable.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public revokedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Revoked Reason",
description: "Reason provided for revoking this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public revokedReason?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.JSON,
title: "Additional Info",
description: "Flexible JSON payload for storing structured metadata.",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public additionalInfo?: JSONObject = undefined;
}

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -505,6 +506,7 @@ export default class TelemetryService extends BaseModel {
Permission.EditTelemetryService,
],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
title: "Service Color",

View File

@@ -30,7 +30,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.Public],
create: [],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],

View File

@@ -0,0 +1,318 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import User from "./User";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import HashedString from "../../Types/HashedString";
import IconProp from "../../Types/Icon/IconProp";
import { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation({
isMasterAdminApiDocs: true,
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-session"))
@Entity({
name: "UserSession",
})
@TableMetadata({
tableName: "UserSession",
singularName: "User Session",
pluralName: "User Sessions",
icon: IconProp.Lock,
tableDescription:
"Active user sessions with refresh tokens and device metadata for enhanced authentication security.",
})
@CurrentUserCanAccessRecordBy("userId")
class UserSession extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User account this session belongs to.",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "delete",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
title: "User ID",
description: "Identifier for the user that owns this session.",
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index({ unique: true })
@TableColumn({
type: TableColumnType.HashedString,
title: "Refresh Token",
description: "Hashed refresh token for this session.",
required: true,
hideColumnInDocumentation: true,
})
@Column({
type: ColumnType.HashedString,
length: ColumnLength.HashedString,
nullable: false,
unique: true,
transformer: HashedString.getDatabaseTransformer(),
})
public refreshToken?: HashedString = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Refresh Token Expires At",
description: "Expiration timestamp for the refresh token.",
required: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Active At",
description: "Last time this session was used.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public lastActiveAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Name",
description: "Friendly name for the device used to sign in.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Type",
description: "Type of device (e.g., desktop, mobile).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceType?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device OS",
description: "Operating system reported for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceOS?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Browser",
description: "Browser or client application used for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceBrowser?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address observed for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "User Agent",
description: "Complete user agent string supplied by the client.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Marks whether the session has been explicitly revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Revoked At",
description: "Timestamp when the session was revoked, if applicable.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public revokedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Revoked Reason",
description: "Optional reason describing why the session was revoked.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public revokedReason?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.JSON,
title: "Additional Info",
description:
"Flexible JSON payload for storing structured session metadata.",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public additionalInfo?: JSONObject = undefined;
}
export default UserSession;

View File

@@ -0,0 +1,96 @@
import AlertInternalNote from "../../Models/DatabaseModels/AlertInternalNote";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import AlertInternalNoteService, {
Service as AlertInternalNoteServiceType,
} from "../Services/AlertInternalNoteService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
export default class AlertInternalNoteAPI extends BaseAPI<
AlertInternalNote,
AlertInternalNoteServiceType
> {
public constructor() {
super(AlertInternalNote, AlertInternalNoteService);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const noteIdParam: string | undefined = req.params["noteId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!noteIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let noteId: ObjectID;
let fileId: ObjectID;
try {
noteId = new ObjectID(noteIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const note: AlertInternalNote | null = await this.service.findOneBy({
query: {
_id: noteId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = note?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -42,17 +42,16 @@ export default class UserAPI extends BaseAPI<
const userPermissions: Array<UserPermission> = (
await this.getPermissionsForTenant(req)
).filter((permission: UserPermission) => {
return (
permission.permission.toString() ===
Permission.ProjectOwner.toString() ||
permission.permission.toString() ===
Permission.CreateBillingPaymentMethod.toString()
);
return [
Permission.ProjectOwner,
Permission.ManageProjectBilling,
Permission.CreateBillingPaymentMethod,
].includes(permission.permission);
});
if (userPermissions.length === 0) {
throw new BadDataException(
"Only Project owner can add payment methods.",
"Only project owners or members with Manage Billing access can add payment methods.",
);
}

View File

@@ -0,0 +1,106 @@
import Incident from "../../Models/DatabaseModels/Incident";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import IncidentService, {
Service as IncidentServiceType,
} from "../Services/IncidentService";
import UserMiddleware from "../Middleware/UserAuthorization";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
export default class IncidentAPI extends BaseAPI<
Incident,
IncidentServiceType
> {
public constructor() {
super(Incident, IncidentService);
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/postmortem/attachment/:projectId/:incidentId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getPostmortemAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getPostmortemAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const projectIdParam: string | undefined = req.params["projectId"];
const incidentIdParam: string | undefined = req.params["incidentId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!projectIdParam || !incidentIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let incidentId: ObjectID;
let fileId: ObjectID;
let projectId: ObjectID;
try {
incidentId = new ObjectID(incidentIdParam);
fileId = new ObjectID(fileIdParam);
projectId = new ObjectID(projectIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const incident: Incident | null = await this.service.findOneBy({
query: {
_id: incidentId,
projectId,
},
select: {
postmortemAttachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
if (!incident) {
throw new NotFoundException("Attachment not found");
}
const attachment: File | undefined = incident.postmortemAttachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -0,0 +1,96 @@
import IncidentInternalNote from "../../Models/DatabaseModels/IncidentInternalNote";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import IncidentInternalNoteService, {
Service as IncidentInternalNoteServiceType,
} from "../Services/IncidentInternalNoteService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
export default class IncidentInternalNoteAPI extends BaseAPI<
IncidentInternalNote,
IncidentInternalNoteServiceType
> {
public constructor() {
super(IncidentInternalNote, IncidentInternalNoteService);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const noteIdParam: string | undefined = req.params["noteId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!noteIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let noteId: ObjectID;
let fileId: ObjectID;
try {
noteId = new ObjectID(noteIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const note: IncidentInternalNote | null = await this.service.findOneBy({
query: {
_id: noteId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = note?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -0,0 +1,96 @@
import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import IncidentPublicNoteService, {
Service as IncidentPublicNoteServiceType,
} from "../Services/IncidentPublicNoteService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
export default class IncidentPublicNoteAPI extends BaseAPI<
IncidentPublicNote,
IncidentPublicNoteServiceType
> {
public constructor() {
super(IncidentPublicNote, IncidentPublicNoteService);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const noteIdParam: string | undefined = req.params["noteId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!noteIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let noteId: ObjectID;
let fileId: ObjectID;
try {
noteId = new ObjectID(noteIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const note: IncidentPublicNote | null = await this.service.findOneBy({
query: {
_id: noteId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = note?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -101,7 +101,48 @@ export default class MicrosoftTeamsAPI {
supportsCalling: false,
supportsVideo: false,
// Provide basic command lists to improve client compatibility (esp. mobile)
commandLists: [],
commandLists: [
{
scopes: ["team", "groupChat", "personal"],
commands: [
{
title: "help",
description:
"Show instructions for interacting with the OneUptime bot.",
},
{
title: "create incident",
description:
"Launch the adaptive card to declare a new incident in OneUptime.",
},
{
title: "create maintenance",
description:
"Open the workflow to schedule maintenance directly from Teams.",
},
{
title: "show active incidents",
description:
"List all ongoing incidents with severity and state context.",
},
{
title: "show scheduled maintenance",
description:
"Display upcoming scheduled maintenance events for the workspace.",
},
{
title: "show ongoing maintenance",
description:
"Surface maintenance windows that are currently in progress.",
},
{
title: "show active alerts",
description:
"Provide a summary of alerts that still require attention.",
},
],
},
],
},
],
permissions: ["identity", "messageTeamMembers"],

View File

@@ -0,0 +1,100 @@
import ScheduledMaintenanceInternalNote from "../../Models/DatabaseModels/ScheduledMaintenanceInternalNote";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import ScheduledMaintenanceInternalNoteService, {
Service as ScheduledMaintenanceInternalNoteServiceType,
} from "../Services/ScheduledMaintenanceInternalNoteService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
export default class ScheduledMaintenanceInternalNoteAPI extends BaseAPI<
ScheduledMaintenanceInternalNote,
ScheduledMaintenanceInternalNoteServiceType
> {
public constructor() {
super(
ScheduledMaintenanceInternalNote,
ScheduledMaintenanceInternalNoteService,
);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const noteIdParam: string | undefined = req.params["noteId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!noteIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let noteId: ObjectID;
let fileId: ObjectID;
try {
noteId = new ObjectID(noteIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const note: ScheduledMaintenanceInternalNote | null =
await this.service.findOneBy({
query: {
_id: noteId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = note?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -0,0 +1,100 @@
import ScheduledMaintenancePublicNote from "../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import ScheduledMaintenancePublicNoteService, {
Service as ScheduledMaintenancePublicNoteServiceType,
} from "../Services/ScheduledMaintenancePublicNoteService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
export default class ScheduledMaintenancePublicNoteAPI extends BaseAPI<
ScheduledMaintenancePublicNote,
ScheduledMaintenancePublicNoteServiceType
> {
public constructor() {
super(
ScheduledMaintenancePublicNote,
ScheduledMaintenancePublicNoteService,
);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const noteIdParam: string | undefined = req.params["noteId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!noteIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let noteId: ObjectID;
let fileId: ObjectID;
try {
noteId = new ObjectID(noteIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const note: ScheduledMaintenancePublicNote | null =
await this.service.findOneBy({
query: {
_id: noteId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = note?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
import StatusPageAnnouncement from "../../Models/DatabaseModels/StatusPageAnnouncement";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import StatusPageAnnouncementService, {
Service as StatusPageAnnouncementServiceType,
} from "../Services/StatusPageAnnouncementService";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import UserMiddleware from "../Middleware/UserAuthorization";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
export default class StatusPageAnnouncementAPI extends BaseAPI<
StatusPageAnnouncement,
StatusPageAnnouncementServiceType
> {
public constructor() {
super(StatusPageAnnouncement, StatusPageAnnouncementService);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:announcementId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getAttachment(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const announcementIdParam: string | undefined =
req.params["announcementId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!announcementIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let announcementId: ObjectID;
let fileId: ObjectID;
try {
announcementId = new ObjectID(announcementIdParam);
fileId = new ObjectID(fileIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const announcement: StatusPageAnnouncement | null =
await this.service.findOneBy({
query: {
_id: announcementId,
},
select: {
attachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
const attachment: File | undefined = announcement?.attachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
}

View File

@@ -0,0 +1,95 @@
import User from "../../Models/DatabaseModels/User";
import NotFoundException from "../../Types/Exception/NotFoundException";
import ObjectID from "../../Types/ObjectID";
import UserService, {
Service as UserServiceType,
} from "../Services/UserService";
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
import logger from "../Utils/Logger";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
const BLANK_PROFILE_PICTURE_PATH: string =
"/usr/src/Common/UI/Images/users/blank-profile.svg";
export default class UserAPI extends BaseAPI<User, UserServiceType> {
public constructor() {
super(User, UserService);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/profile-picture/:userId`,
async (req: ExpressRequest, res: ExpressResponse) => {
const userIdParam: string | undefined = req.params["userId"];
if (!userIdParam) {
return this.sendBlankProfile(req, res);
}
let userId: ObjectID;
try {
userId = new ObjectID(userIdParam);
} catch {
return this.sendBlankProfile(req, res);
}
try {
const profilePictureSelect: {
profilePictureFile: {
_id: boolean;
file: boolean;
fileType: boolean;
name: boolean;
};
} = {
profilePictureFile: {
_id: true,
file: true,
fileType: true,
name: true,
},
};
const userById: User | null = await UserService.findOneBy({
query: {
_id: userId,
},
select: profilePictureSelect,
props: {
isRoot: true,
},
});
if (userById && userById.profilePictureFile) {
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(
req,
res,
userById.profilePictureFile,
);
}
return this.sendBlankProfile(req, res);
} catch (error) {
logger.error(error);
return this.sendBlankProfile(req, res);
}
},
);
}
private sendBlankProfile(req: ExpressRequest, res: ExpressResponse): void {
Response.setNoCacheHeaders(res);
try {
Response.sendFileByPath(req, res, BLANK_PROFILE_PICTURE_PATH);
} catch (error) {
logger.error(error);
Response.sendErrorResponse(
req,
res,
new NotFoundException("User profile picture not found"),
);
}
}
}

View File

@@ -44,6 +44,8 @@ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
"DISABLE_TELEMETRY",
"SLACK_APP_CLIENT_ID",
"MICROSOFT_TEAMS_APP_CLIENT_ID",
"CAPTCHA_ENABLED",
"CAPTCHA_SITE_KEY",
];
const FRONTEND_ENV_ALLOW_PREFIXES: Array<string> = [
@@ -324,6 +326,13 @@ export const Host: string = process.env["HOST"] || "";
export const ProvisionSsl: boolean = process.env["PROVISION_SSL"] === "true";
export const CaptchaEnabled: boolean =
process.env["CAPTCHA_ENABLED"] === "true";
export const CaptchaSecretKey: string = process.env["CAPTCHA_SECRET_KEY"] || "";
export const CaptchaSiteKey: string = process.env["CAPTCHA_SITE_KEY"] || "";
export const WorkflowScriptTimeoutInMS: number = process.env[
"WORKFLOW_SCRIPT_TIMEOUT_IN_MS"
]
@@ -446,6 +455,8 @@ export const MicrosoftTeamsAppClientId: string | null =
process.env["MICROSOFT_TEAMS_APP_CLIENT_ID"] || null;
export const MicrosoftTeamsAppClientSecret: string | null =
process.env["MICROSOFT_TEAMS_APP_CLIENT_SECRET"] || null;
export const MicrosoftTeamsAppTenantId: string | null =
process.env["MICROSOFT_TEAMS_APP_TENANT_ID"] || null;
// VAPID Configuration for Web Push Notifications
export const VapidPublicKey: string | undefined =

View File

@@ -0,0 +1,91 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1762890441920 implements MigrationInterface {
public name = "MigrationName1762890441920";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "StatusPagePrivateUserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_ac5f4c13d6bc9696cbfb8e5a79" ON "StatusPagePrivateUserSession" ("projectId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
);
await queryRunner.query(
`CREATE TABLE "UserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "userId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_d66bd8342b0005c7192bdb17efc" UNIQUE ("refreshToken"), CONSTRAINT "PK_9dcd180f25755bab5fcebcbeb14" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_7353eaf92987aeaf38c2590e94" ON "UserSession" ("userId") `,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_d66bd8342b0005c7192bdb17ef"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7353eaf92987aeaf38c2590e94"`,
);
await queryRunner.query(`DROP TABLE "UserSession"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_ac5f4c13d6bc9696cbfb8e5a79"`,
);
await queryRunner.query(`DROP TABLE "StatusPagePrivateUserSession"`);
}
}

View File

@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1763471659817 implements MigrationInterface {
public name = "MigrationName1763471659817";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "IncidentInternalNoteFile" ("incidentInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1e97a749db84f9dc65ee162dd6b" PRIMARY KEY ("incidentInternalNoteId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_0edb0291ff3e97197269d77dc4" ON "IncidentInternalNoteFile" ("incidentInternalNoteId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_b30b49d21a553c06bd0ff3acf5" ON "IncidentInternalNoteFile" ("fileId") `,
);
await queryRunner.query(
`CREATE TABLE "IncidentPublicNoteFile" ("incidentPublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_42d2fe75b663f8fa20421f31e78" PRIMARY KEY ("incidentPublicNoteId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_e5c4a5671b2bb51a9918f1f203" ON "IncidentPublicNoteFile" ("incidentPublicNoteId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_81a5bc92f59cb5577746ee51ba" ON "IncidentPublicNoteFile" ("fileId") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_0edb0291ff3e97197269d77dc48" FOREIGN KEY ("incidentInternalNoteId") REFERENCES "IncidentInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d" FOREIGN KEY ("incidentPublicNoteId") REFERENCES "IncidentPublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf"`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d"`,
);
await queryRunner.query(
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f"`,
);
await queryRunner.query(
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_0edb0291ff3e97197269d77dc48"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_81a5bc92f59cb5577746ee51ba"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_e5c4a5671b2bb51a9918f1f203"`,
);
await queryRunner.query(`DROP TABLE "IncidentPublicNoteFile"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_b30b49d21a553c06bd0ff3acf5"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_0edb0291ff3e97197269d77dc4"`,
);
await queryRunner.query(`DROP TABLE "IncidentInternalNoteFile"`);
}
}

View File

@@ -0,0 +1,81 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1763477560906 implements MigrationInterface {
public name = "MigrationName1763477560906";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_fddb744dc7cf400724befe5ba91" PRIMARY KEY ("scheduledMaintenanceInternalNoteId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_ac92a60535a6d598c9619fd199" ON "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_daee340befeece208b507a4242" ON "ScheduledMaintenanceInternalNoteFile" ("fileId") `,
);
await queryRunner.query(
`CREATE TABLE "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_373f78b83aa76e5250df8ebaed7" PRIMARY KEY ("scheduledMaintenancePublicNoteId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_af6905f89ca8108ed0f478fd37" ON "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_f09af6332e0b89f134472f0442" ON "ScheduledMaintenancePublicNoteFile" ("fileId") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_ac92a60535a6d598c9619fd1999" FOREIGN KEY ("scheduledMaintenanceInternalNoteId") REFERENCES "ScheduledMaintenanceInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_daee340befeece208b507a42423" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_af6905f89ca8108ed0f478fd376" FOREIGN KEY ("scheduledMaintenancePublicNoteId") REFERENCES "ScheduledMaintenancePublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_f09af6332e0b89f134472f0442a" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_f09af6332e0b89f134472f0442a"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_af6905f89ca8108ed0f478fd376"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_daee340befeece208b507a42423"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_ac92a60535a6d598c9619fd1999"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_f09af6332e0b89f134472f0442"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_af6905f89ca8108ed0f478fd37"`,
);
await queryRunner.query(`DROP TABLE "ScheduledMaintenancePublicNoteFile"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_daee340befeece208b507a4242"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_ac92a60535a6d598c9619fd199"`,
);
await queryRunner.query(
`DROP TABLE "ScheduledMaintenanceInternalNoteFile"`,
);
}
}

View File

@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1763480947474 implements MigrationInterface {
public name = "MigrationName1763480947474";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "StatusPageAnnouncementFile" ("statusPageAnnouncementId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1323a0215e608ece58a96816134" PRIMARY KEY ("statusPageAnnouncementId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_b152a77a26a67d2e76160ba15e" ON "StatusPageAnnouncementFile" ("statusPageAnnouncementId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_2f78e2d073bf58013c962ce482" ON "StatusPageAnnouncementFile" ("fileId") `,
);
await queryRunner.query(
`CREATE TABLE "AlertInternalNoteFile" ("alertInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_a5370c68590b3db5c3635d364aa" PRIMARY KEY ("alertInternalNoteId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_09507cdab877a482edcc4c0593" ON "AlertInternalNoteFile" ("alertInternalNoteId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_77dc8a31bd4ebb0882450abdde" ON "AlertInternalNoteFile" ("fileId") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3" FOREIGN KEY ("statusPageAnnouncementId") REFERENCES "StatusPageAnnouncement"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_2f78e2d073bf58013c962ce4827" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_09507cdab877a482edcc4c05933" FOREIGN KEY ("alertInternalNoteId") REFERENCES "AlertInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9"`,
);
await queryRunner.query(
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_09507cdab877a482edcc4c05933"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_2f78e2d073bf58013c962ce4827"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_77dc8a31bd4ebb0882450abdde"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_09507cdab877a482edcc4c0593"`,
);
await queryRunner.query(`DROP TABLE "AlertInternalNoteFile"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_2f78e2d073bf58013c962ce482"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_b152a77a26a67d2e76160ba15e"`,
);
await queryRunner.query(`DROP TABLE "StatusPageAnnouncementFile"`);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1763643080445 implements MigrationInterface {
public name = "MigrationName1763643080445";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "enableMasterPassword" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "masterPassword" character varying(64)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "masterPassword"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "enableMasterPassword"`,
);
}
}

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1764324618043 implements MigrationInterface {
public name = "MigrationName1764324618043";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" ADD "declaredAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`UPDATE "Incident" SET "declaredAt" = "createdAt" WHERE "declaredAt" IS NULL`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ALTER COLUMN "declaredAt" SET DEFAULT now()`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ALTER COLUMN "declaredAt" SET NOT NULL`,
);
await queryRunner.query(
`CREATE INDEX "IDX_b26979b9f119310661734465a4" ON "Incident" ("declaredAt") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_b26979b9f119310661734465a4"`,
);
await queryRunner.query(`ALTER TABLE "Incident" DROP COLUMN "declaredAt"`);
}
}

View File

@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1764762146063 implements MigrationInterface {
public name = "MigrationName1764762146063";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "IncidentPostmortemAttachmentFile" ("incidentId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_40b17c7d5bcfbde48d7ebab4130" PRIMARY KEY ("incidentId", "fileId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_62b9c09c42e05df3f134aa14a4" ON "IncidentPostmortemAttachmentFile" ("incidentId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_7e09116a3b9672622bba9f8b2e" ON "IncidentPostmortemAttachmentFile" ("fileId") `,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "showPostmortemOnStatusPage" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPostmortemAttachmentFile" ADD CONSTRAINT "FK_62b9c09c42e05df3f134aa14a46" FOREIGN KEY ("incidentId") REFERENCES "Incident"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPostmortemAttachmentFile" ADD CONSTRAINT "FK_7e09116a3b9672622bba9f8b2e3" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "IncidentPostmortemAttachmentFile" DROP CONSTRAINT "FK_7e09116a3b9672622bba9f8b2e3"`,
);
await queryRunner.query(
`ALTER TABLE "IncidentPostmortemAttachmentFile" DROP CONSTRAINT "FK_62b9c09c42e05df3f134aa14a46"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "showPostmortemOnStatusPage"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7e09116a3b9672622bba9f8b2e"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_62b9c09c42e05df3f134aa14a4"`,
);
await queryRunner.query(`DROP TABLE "IncidentPostmortemAttachmentFile"`);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1764767371788 implements MigrationInterface {
public name = "MigrationName1764767371788";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1764789433216 implements MigrationInterface {
public name = "MigrationName1764789433216";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" ADD "subscriberNotificationStatusOnPostmortemPublished" character varying NOT NULL DEFAULT 'Pending'`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "subscriberNotificationStatusMessageOnPostmortemPublished" text`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "notifySubscribersOnPostmortemPublished" boolean NOT NULL DEFAULT true`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "notifySubscribersOnPostmortemPublished"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "subscriberNotificationStatusMessageOnPostmortemPublished"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "subscriberNotificationStatusOnPostmortemPublished"`,
);
}
}

View File

@@ -181,6 +181,15 @@ import { MigrationName1761232578396 } from "./1761232578396-MigrationName";
import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
import { MigrationName1763471659817 } from "./1763471659817-MigrationName";
import { MigrationName1763477560906 } from "./1763477560906-MigrationName";
import { MigrationName1763480947474 } from "./1763480947474-MigrationName";
import { MigrationName1763643080445 } from "./1763643080445-MigrationName";
import { MigrationName1764324618043 } from "./1764324618043-MigrationName";
import { MigrationName1764762146063 } from "./1764762146063-MigrationName";
import { MigrationName1764767371788 } from "./1764767371788-MigrationName";
import { MigrationName1764789433216 } from "./1764789433216-MigrationName";
export default [
InitialMigration,
@@ -366,4 +375,13 @@ export default [
MigrationName1761834523183,
MigrationName1762181014879,
MigrationName1762554602716,
MigrationName1762890441920,
MigrationName1763471659817,
MigrationName1763477560906,
MigrationName1763480947474,
MigrationName1763643080445,
MigrationName1764324618043,
MigrationName1764762146063,
MigrationName1764767371788,
MigrationName1764789433216,
];

View File

@@ -24,7 +24,9 @@ export default class ProjectMiddleware {
@CaptureSpan()
public static getProjectId(req: ExpressRequest): ObjectID | null {
let projectId: ObjectID | null = null;
if (req.params && req.params["tenantid"]) {
if (req.params && req.params["projectId"]) {
projectId = new ObjectID(req.params["projectId"]);
} else if (req.params && req.params["tenantid"]) {
projectId = new ObjectID(req.params["tenantid"]);
} else if (req.query && req.query["tenantid"]) {
projectId = new ObjectID(req.query["tenantid"] as string);

View File

@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import Alert from "../../Models/DatabaseModels/Alert";
import AlertService from "./AlertService";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import File from "../../Models/DatabaseModels/File";
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
alertId: ObjectID;
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
const file: File = new File();
file.id = fileId;
return file;
},
);
}
return this.create({
data: internalNote,
props: {
@@ -50,6 +63,11 @@ export class Service extends DatabaseService<Model> {
alertId: alertId,
});
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
createdItem.id!,
"/alert-internal-note/attachment",
);
await AlertFeedService.createAlertFeedItem({
alertId: createdItem.alertId!,
projectId: createdItem.projectId!,
@@ -59,7 +77,7 @@ export class Service extends DatabaseService<Model> {
feedInfoInMarkdown: `📄 posted **private note** for this [Alert ${alertNumber}](${(await AlertService.getAlertLinkInDashboard(createdItem.projectId!, alertId)).toString()}):
${createdItem.note}
${(createdItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -104,6 +122,10 @@ ${createdItem.note}
for (const updatedItem of updatedItems) {
const alert: Alert = updatedItem.alert!;
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
updatedItem.id!,
"/alert-internal-note/attachment",
);
await AlertFeedService.createAlertFeedItem({
alertId: updatedItem.alertId!,
projectId: updatedItem.projectId!,
@@ -113,7 +135,7 @@ ${createdItem.note}
feedInfoInMarkdown: `📄 updated **Private Note** for this [Alert ${alert.alertNumber}](${(await AlertService.getAlertLinkInDashboard(alert.projectId!, alert.id!)).toString()})
${updatedItem.note}
${(updatedItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -124,6 +146,57 @@ ${updatedItem.note}
}
return onUpdate;
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,
): Promise<string> {
if (!modelId) {
return "";
}
const noteWithAttachments: Model | null = await this.findOneById({
id: modelId,
select: {
attachments: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!noteWithAttachments || !noteWithAttachments.attachments) {
return "";
}
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
.map((file: File) => {
if (file.id) {
return file.id;
}
if (file._id) {
return new ObjectID(file._id);
}
return null;
})
.filter((id: ObjectID | null): id is ObjectID => {
return Boolean(id);
});
if (!attachmentIds.length) {
return "";
}
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
modelId,
attachmentIds,
attachmentApiPath,
});
}
}
export default new Service();

View File

@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import IncidentService from "./IncidentService";
import Incident from "../../Models/DatabaseModels/Incident";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import File from "../../Models/DatabaseModels/File";
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
incidentId: ObjectID;
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
const file: File = new File();
file.id = fileId;
return file;
},
);
}
return this.create({
data: internalNote,
props: {
@@ -51,6 +64,11 @@ export class Service extends DatabaseService<Model> {
incidentId: incidentId,
});
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
createdItem.id!,
"/incident-internal-note/attachment",
);
await IncidentFeedService.createIncidentFeedItem({
incidentId: createdItem.incidentId!,
projectId: createdItem.projectId!,
@@ -60,7 +78,7 @@ export class Service extends DatabaseService<Model> {
feedInfoInMarkdown: `📄 posted **private note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(createdItem.projectId!, incidentId)).toString()}):
${createdItem.note}
${(createdItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -105,6 +123,11 @@ ${createdItem.note}
for (const updatedItem of updatedItems) {
const incident: Incident = updatedItem.incident!;
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
updatedItem.id!,
"/incident-internal-note/attachment",
);
await IncidentFeedService.createIncidentFeedItem({
incidentId: updatedItem.incidentId!,
projectId: updatedItem.projectId!,
@@ -114,7 +137,7 @@ ${createdItem.note}
feedInfoInMarkdown: `📄 updated **Private Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
${updatedItem.note}
${(updatedItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -125,6 +148,57 @@ ${updatedItem.note}
}
return onUpdate;
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,
): Promise<string> {
if (!modelId) {
return "";
}
const noteWithAttachments: Model | null = await this.findOneById({
id: modelId,
select: {
attachments: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!noteWithAttachments || !noteWithAttachments.attachments) {
return "";
}
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
.map((file: File) => {
if (file.id) {
return file.id;
}
if (file._id) {
return new ObjectID(file._id);
}
return null;
})
.filter((id: ObjectID | null): id is ObjectID => {
return Boolean(id);
});
if (!attachmentIds.length) {
return "";
}
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
modelId,
attachmentIds,
attachmentApiPath,
});
}
}
export default new Service();

View File

@@ -12,6 +12,8 @@ import IncidentService from "./IncidentService";
import Incident from "../../Models/DatabaseModels/Incident";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import File from "../../Models/DatabaseModels/File";
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -24,6 +26,7 @@ export class Service extends DatabaseService<Model> {
incidentId: ObjectID;
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
}): Promise<Model> {
const publicNote: Model = new Model();
publicNote.createdByUserId = data.userId;
@@ -32,6 +35,16 @@ export class Service extends DatabaseService<Model> {
publicNote.note = data.note;
publicNote.postedAt = OneUptimeDate.getCurrentDate();
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
publicNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
const file: File = new File();
file.id = fileId;
return file;
},
);
}
return this.create({
data: publicNote,
props: {
@@ -84,6 +97,11 @@ export class Service extends DatabaseService<Model> {
incidentId: incidentId,
});
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
createdItem.id!,
"/incident-public-note/attachment",
);
await IncidentFeedService.createIncidentFeedItem({
incidentId: createdItem.incidentId!,
projectId: createdItem.projectId!,
@@ -92,7 +110,7 @@ export class Service extends DatabaseService<Model> {
userId: userId || undefined,
feedInfoInMarkdown: `📄 posted **public note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(projectId!, incidentId!)).toString()}) on status page:
${createdItem.note}
${(createdItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -138,6 +156,11 @@ ${createdItem.note}
for (const updatedItem of updatedItems) {
const incident: Incident = updatedItem.incident!;
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
updatedItem.id!,
"/incident-public-note/attachment",
);
await IncidentFeedService.createIncidentFeedItem({
incidentId: updatedItem.incidentId!,
projectId: updatedItem.projectId!,
@@ -147,7 +170,7 @@ ${createdItem.note}
feedInfoInMarkdown: `📄 updated **Public Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
${updatedItem.note}
${(updatedItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -158,6 +181,57 @@ ${updatedItem.note}
}
return onUpdate;
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,
): Promise<string> {
if (!modelId) {
return "";
}
const noteWithAttachments: Model | null = await this.findOneById({
id: modelId,
select: {
attachments: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!noteWithAttachments || !noteWithAttachments.attachments) {
return "";
}
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
.map((file: File) => {
if (file.id) {
return file.id;
}
if (file._id) {
return new ObjectID(file._id);
}
return null;
})
.filter((id: ObjectID | null): id is ObjectID => {
return Boolean(id);
});
if (!attachmentIds.length) {
return "";
}
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
modelId,
attachmentIds,
attachmentApiPath,
});
}
}
export default new Service();

View File

@@ -480,6 +480,14 @@ export class Service extends DatabaseService<Model> {
const projectId: ObjectID =
createBy.props.tenantId || createBy.data.projectId!;
if (!createBy.data.declaredAt) {
createBy.data.declaredAt = OneUptimeDate.getCurrentDate();
} else {
createBy.data.declaredAt = OneUptimeDate.fromString(
createBy.data.declaredAt as Date,
);
}
// Determine the initial incident state
let initialIncidentStateId: ObjectID | undefined = undefined;
@@ -975,6 +983,7 @@ ${incident.remediationNotes || "No remediation notes provided."}
notifyOwners: false,
rootCause: createdItem.rootCause,
stateChangeLog: createdItem.createdStateLog,
timelineStartsAt: createdItem.declaredAt,
props: {
isRoot: true,
},
@@ -1345,6 +1354,19 @@ ${incident.remediationNotes || "No remediation notes provided."}
sendWorkspaceNotification: true,
},
});
// Set subscriber notification status to Pending so the cron job will send notifications
await this.updateOneById({
id: incidentId,
data: {
subscriberNotificationStatusOnPostmortemPublished:
StatusPageSubscriberNotificationStatus.Pending,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
}
let shouldAddIncidentFeed: boolean = false;
@@ -1790,6 +1812,7 @@ ${incidentSeverity.name}
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
projectId: true,
monitors: {
_id: true,
@@ -1821,6 +1844,18 @@ ${incidentSeverity.name}
incident.monitors,
);
}
if (incident.projectId && incident.id) {
await MetricService.deleteBy({
query: {
projectId: incident.projectId,
serviceId: incident.id,
},
props: {
isRoot: true,
},
});
}
}
}
@@ -1838,6 +1873,7 @@ ${incidentSeverity.name}
rootCause: string | undefined;
stateChangeLog: JSONObject | undefined;
props: DatabaseCommonInteractionProps | undefined;
timelineStartsAt?: Date | string | undefined;
}): Promise<void> {
const {
projectId,
@@ -1849,8 +1885,13 @@ ${incidentSeverity.name}
rootCause,
stateChangeLog,
props,
timelineStartsAt,
} = data;
const declaredTimelineStart: Date | undefined = timelineStartsAt
? OneUptimeDate.fromString(timelineStartsAt as Date)
: undefined;
// get last monitor status timeline.
const lastIncidentStatusTimeline: IncidentStateTimeline | null =
await IncidentStateTimelineService.findOneBy({
@@ -1888,6 +1929,10 @@ ${incidentSeverity.name}
statusTimeline.shouldStatusPageSubscribersBeNotified =
shouldNotifyStatusPageSubscribers;
if (!lastIncidentStatusTimeline && declaredTimelineStart) {
statusTimeline.startsAt = declaredTimelineStart;
}
// Map boolean to enum value
statusTimeline.subscriberNotificationStatus = isSubscribersNotified
? StatusPageSubscriberNotificationStatus.Success
@@ -1914,6 +1959,7 @@ ${incidentSeverity.name}
id: data.incidentId,
select: {
projectId: true,
declaredAt: true,
monitors: {
_id: true,
name: true,
@@ -1970,6 +2016,7 @@ ${incidentSeverity.name}
await MetricService.deleteBy({
query: {
projectId: incident.projectId,
serviceId: data.incidentId,
},
props: {
@@ -1983,6 +2030,7 @@ ${incidentSeverity.name}
const incidentStartsAt: Date =
firstIncidentStateTimeline?.startsAt ||
incident.declaredAt ||
incident.createdAt ||
OneUptimeDate.getCurrentDate();
@@ -2075,6 +2123,7 @@ ${incidentSeverity.name}
timeToAcknowledgeMetric.time =
ackIncidentStateTimeline?.startsAt ||
incident.declaredAt ||
incident.createdAt ||
OneUptimeDate.getCurrentDate();
timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
@@ -2140,6 +2189,7 @@ ${incidentSeverity.name}
timeToResolveMetric.time =
resolvedIncidentStateTimeline?.startsAt ||
incident.declaredAt ||
incident.createdAt ||
OneUptimeDate.getCurrentDate();
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
@@ -2200,6 +2250,7 @@ ${incidentSeverity.name}
incidentDurationMetric.time =
lastIncidentStateTimeline?.startsAt ||
incident.declaredAt ||
incident.createdAt ||
OneUptimeDate.getCurrentDate();
incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(

View File

@@ -106,6 +106,7 @@ import StatusPageHistoryChartBarColorRuleService from "./StatusPageHistoryChartB
import StatusPageOwnerTeamService from "./StatusPageOwnerTeamService";
import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
import StatusPagePrivateUserService from "./StatusPagePrivateUserService";
import StatusPagePrivateUserSessionService from "./StatusPagePrivateUserSessionService";
import StatusPageResourceService from "./StatusPageResourceService";
// Status Page
import StatusPageService from "./StatusPageService";
@@ -125,6 +126,7 @@ import UserNotificationSettingService from "./UserNotificationSettingService";
import UserOnCallLogService from "./UserOnCallLogService";
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
import UserService from "./UserService";
import UserSessionService from "./UserSessionService";
import UserTotpAuthService from "./UserTotpAuthService";
import UserWebAuthnService from "./UserWebAuthnService";
import UserSmsService from "./UserSmsService";
@@ -266,6 +268,7 @@ const services: Array<BaseService> = [
StatusPageOwnerTeamService,
StatusPageOwnerUserService,
StatusPagePrivateUserService,
StatusPagePrivateUserSessionService,
StatusPageResourceService,
StatusPageService,
StatusPageSsoService,
@@ -278,6 +281,7 @@ const services: Array<BaseService> = [
TeamService,
UserService,
UserSessionService,
UserCallService,
UserEmailService,
UserNotificationRuleService,

View File

@@ -63,14 +63,13 @@ import MonitorFeedService from "./MonitorFeedService";
import { MonitorFeedEventType } from "../../Models/DatabaseModels/MonitorFeed";
import { Gray500, Green500 } from "../../Types/BrandColors";
import LabelService from "./LabelService";
import QueryOperator from "../../Types/BaseDatabase/QueryOperator";
import { FindWhere } from "../../Types/BaseDatabase/Query";
import logger from "../Utils/Logger";
import PushNotificationUtil from "../Utils/PushNotificationUtil";
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
import Project from "../../Models/DatabaseModels/Project";
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
import MetricService from "./MetricService";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -136,12 +135,26 @@ export class Service extends DatabaseService<Model> {
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
if (deleteBy.query._id) {
// delete all the status page resource for this monitor.
const monitorsPendingDeletion: Array<Model> = await this.findBy({
query: deleteBy.query,
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
projectId: true,
},
props: deleteBy.props,
});
for (const monitor of monitorsPendingDeletion) {
if (!monitor.id) {
continue;
}
// delete all the status page resources for this monitor.
await StatusPageResourceService.deleteBy({
query: {
monitorId: new ObjectID(deleteBy.query._id as string),
monitorId: monitor.id,
},
limit: LIMIT_MAX,
skip: 0,
@@ -150,37 +163,19 @@ export class Service extends DatabaseService<Model> {
},
});
let projectId: FindWhere<ObjectID> | QueryOperator<ObjectID> | undefined =
deleteBy.query.projectId || deleteBy.props.tenantId;
const projectId: ObjectID | undefined = monitor.projectId as
| ObjectID
| undefined;
if (!projectId) {
// fetch this monitor from the database to get the projectId.
const monitor: Model | null = await this.findOneById({
id: new ObjectID(deleteBy.query._id as string) as ObjectID,
select: {
projectId: true,
},
props: {
isRoot: true,
},
});
if (!monitor) {
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
if (!monitor.id) {
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
projectId = monitor.projectId!;
continue;
}
try {
await WorkspaceNotificationRuleService.archiveWorkspaceChannels({
projectId: projectId as ObjectID,
projectId: projectId,
notificationFor: {
monitorId: new ObjectID(deleteBy.query._id as string) as ObjectID,
monitorId: monitor.id,
},
sendMessageBeforeArchiving: {
_type: "WorkspacePayloadMarkdown",
@@ -189,12 +184,17 @@ export class Service extends DatabaseService<Model> {
});
} catch (error) {
logger.error(
`Error while archiving workspace channels for monitor ${deleteBy.query._id}: ${error}`,
`Error while archiving workspace channels for monitor ${monitor.id?.toString()}: ${error}`,
);
}
}
return { deleteBy, carryForward: null };
return {
deleteBy,
carryForward: {
monitors: monitorsPendingDeletion,
},
};
}
@CaptureSpan()
@@ -208,6 +208,24 @@ export class Service extends DatabaseService<Model> {
);
}
if (onDelete.carryForward && onDelete.carryForward.monitors) {
for (const monitor of onDelete.carryForward.monitors as Array<Model>) {
if (!monitor.projectId || !monitor.id) {
continue;
}
await MetricService.deleteBy({
query: {
projectId: monitor.projectId,
serviceId: monitor.id,
},
props: {
isRoot: true,
},
});
}
}
return onDelete;
}

View File

@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import File from "../../Models/DatabaseModels/File";
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
scheduledMaintenanceId: ObjectID;
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
const file: File = new File();
file.id = fileId;
return file;
},
);
}
return this.create({
data: internalNote,
props: {
@@ -52,6 +65,11 @@ export class Service extends DatabaseService<Model> {
scheduledMaintenanceId: scheduledMaintenanceId,
});
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
createdItem.id!,
"/scheduled-maintenance-internal-note/attachment",
);
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
projectId: createdItem.projectId!,
@@ -62,7 +80,7 @@ export class Service extends DatabaseService<Model> {
feedInfoInMarkdown: `📄 posted **private note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(createdItem.projectId!, scheduledMaintenanceId)).toString()}):
${createdItem.note}
${(createdItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -109,6 +127,11 @@ ${createdItem.note}
const scheduledMaintenance: ScheduledMaintenance =
updatedItem.scheduledMaintenance!;
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
updatedItem.id!,
"/scheduled-maintenance-internal-note/attachment",
);
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
{
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
@@ -120,7 +143,7 @@ ${createdItem.note}
feedInfoInMarkdown: `📄 updated **Private Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
${updatedItem.note}
${(updatedItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -132,6 +155,57 @@ ${updatedItem.note}
}
return onUpdate;
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,
): Promise<string> {
if (!modelId) {
return "";
}
const noteWithAttachments: Model | null = await this.findOneById({
id: modelId,
select: {
attachments: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!noteWithAttachments || !noteWithAttachments.attachments) {
return "";
}
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
.map((file: File) => {
if (file.id) {
return file.id;
}
if (file._id) {
return new ObjectID(file._id);
}
return null;
})
.filter((id: ObjectID | null): id is ObjectID => {
return Boolean(id);
});
if (!attachmentIds.length) {
return "";
}
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
modelId,
attachmentIds,
attachmentApiPath,
});
}
}
export default new Service();

View File

@@ -12,6 +12,8 @@ import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import File from "../../Models/DatabaseModels/File";
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -63,6 +65,11 @@ export class Service extends DatabaseService<Model> {
scheduledMaintenanceId: scheduledMaintenanceId,
});
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
createdItem.id!,
"/scheduled-maintenance-public-note/attachment",
);
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
projectId: createdItem.projectId!,
@@ -72,7 +79,7 @@ export class Service extends DatabaseService<Model> {
userId: userId || undefined,
feedInfoInMarkdown: `📄 posted **public note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(projectId!, scheduledMaintenanceId!)).toString()}) on status page:
${createdItem.note}
${(createdItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -119,6 +126,11 @@ ${createdItem.note}
const scheduledMaintenance: ScheduledMaintenance =
updatedItem.scheduledMaintenance!;
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
updatedItem.id!,
"/scheduled-maintenance-public-note/attachment",
);
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
{
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
@@ -130,7 +142,7 @@ ${createdItem.note}
feedInfoInMarkdown: `📄 updated **Public Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
${updatedItem.note}
${(updatedItem.note || "") + attachmentsMarkdown}
`,
workspaceNotification: {
sendWorkspaceNotification: true,
@@ -149,6 +161,7 @@ ${updatedItem.note}
scheduledMaintenanceId: ObjectID;
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
}): Promise<Model> {
const publicNote: Model = new Model();
publicNote.createdByUserId = data.userId;
@@ -157,6 +170,16 @@ ${updatedItem.note}
publicNote.note = data.note;
publicNote.postedAt = OneUptimeDate.getCurrentDate();
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
publicNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
const file: File = new File();
file.id = fileId;
return file;
},
);
}
return this.create({
data: publicNote,
props: {
@@ -164,6 +187,57 @@ ${updatedItem.note}
},
});
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,
): Promise<string> {
if (!modelId) {
return "";
}
const noteWithAttachments: Model | null = await this.findOneById({
id: modelId,
select: {
attachments: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!noteWithAttachments || !noteWithAttachments.attachments) {
return "";
}
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
.map((file: File) => {
if (file.id) {
return file.id;
}
if (file._id) {
return new ObjectID(file._id);
}
return null;
})
.filter((id: ObjectID | null): id is ObjectID => {
return Boolean(id);
});
if (!attachmentIds.length) {
return "";
}
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
modelId,
attachmentIds,
attachmentApiPath,
});
}
}
export default new Service();

View File

@@ -27,7 +27,7 @@ import User from "../../Models/DatabaseModels/User";
import Recurring from "../../Types/Events/Recurring";
import OneUptimeDate from "../../Types/Date";
import UpdateBy from "../Types/Database/UpdateBy";
import { FileRoute } from "../../ServiceRoute";
import { StatusPageApiRoute } from "../../ServiceRoute";
import Dictionary from "../../Types/Dictionary";
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
import SMS from "../../Types/SMS/SMS";
@@ -193,6 +193,13 @@ export class Service extends DatabaseService<Model> {
const statusPageName: string =
statuspage.pageTitle || statuspage.name || "Status Page";
const scheduledEventDetailsUrl: string =
event.id && statusPageURL
? URL.fromString(statusPageURL)
.addRoute(`/scheduled-events/${event.id.toString()}`)
.toString()
: statusPageURL;
// Send email to Email subscribers.
const resourcesAffected: string =
@@ -280,6 +287,8 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
if (subscriber.subscriberEmail) {
// send email here.
const statusPageIdString: string | null =
statuspage.id?.toString() || statuspage._id?.toString() || null;
MailService.sendMail(
{
@@ -289,12 +298,14 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
vars: {
statusPageName: statusPageName,
statusPageUrl: statusPageURL,
logoUrl: statuspage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statuspage.logoFileId)
.toString()
: "",
detailsUrl: scheduledEventDetailsUrl,
logoUrl:
statuspage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
isPublicStatusPage: statuspage.isPublicStatusPage
? "true"
: "false",
@@ -374,27 +385,60 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
(updateBy.data.startsAt as Date) ||
(scheduledMaintenance.startsAt! as Date);
const notificationSettings: Array<Recurring> =
(updateBy.data
.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>) ||
(scheduledMaintenance.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>);
let notificationSettings: Array<Recurring> | null = null;
const updatedNotificationSettings: Array<Recurring> | null | undefined =
updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent as
| Array<Recurring>
| null
| undefined;
if (
updatedNotificationSettings !== null &&
updatedNotificationSettings !== undefined
) {
notificationSettings = updatedNotificationSettings;
} else {
const existingNotificationSettings:
| Array<Recurring>
| null
| undefined =
scheduledMaintenance.sendSubscriberNotificationsOnBeforeTheEvent as
| Array<Recurring>
| null
| undefined;
if (
existingNotificationSettings !== null &&
existingNotificationSettings !== undefined
) {
notificationSettings = existingNotificationSettings;
}
}
logger.debug(
`Using startsAt: ${startsAt} and notificationSettings: ${JSON.stringify(notificationSettings)}`,
);
const nextTimeToNotifyBeforeTheEvent: Date | null =
this.getNextTimeToNotify({
eventScheduledDate: startsAt,
sendSubscriberNotifiationsOn: notificationSettings,
});
if (!notificationSettings || notificationSettings.length === 0) {
logger.debug(
"No subscriber notification schedule configured. Clearing nextSubscriberNotificationBeforeTheEventAt.",
);
updateBy.data.nextSubscriberNotificationBeforeTheEventAt = null;
} else {
const nextTimeToNotifyBeforeTheEvent: Date | null =
this.getNextTimeToNotify({
eventScheduledDate: startsAt,
sendSubscriberNotifiationsOn: notificationSettings,
});
updateBy.data.nextSubscriberNotificationBeforeTheEventAt =
nextTimeToNotifyBeforeTheEvent;
updateBy.data.nextSubscriberNotificationBeforeTheEventAt =
nextTimeToNotifyBeforeTheEvent;
logger.debug(
`nextSubscriberNotificationBeforeTheEventAt set to: ${nextTimeToNotifyBeforeTheEvent}`,
);
logger.debug(
`nextSubscriberNotificationBeforeTheEventAt set to: ${nextTimeToNotifyBeforeTheEvent}`,
);
}
}
// Set notification status based on shouldStatusPageSubscribersBeNotifiedOnEventCreated if it's being updated
@@ -472,7 +516,7 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
public getNextTimeToNotify(data: {
eventScheduledDate: Date;
sendSubscriberNotifiationsOn: Array<Recurring>;
sendSubscriberNotifiationsOn?: Array<Recurring> | null | undefined;
}): Date | null {
let recurringDate: Date | null = null;
@@ -483,7 +527,23 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
`Calculating next time to notify for event scheduled date: ${data.eventScheduledDate}`,
);
for (const recurringItem of data.sendSubscriberNotifiationsOn) {
const notificationSchedules: Array<Recurring> = Array.isArray(
data.sendSubscriberNotifiationsOn,
)
? (data.sendSubscriberNotifiationsOn as Array<Recurring>)
: [];
if (notificationSchedules.length === 0) {
logger.debug(
"No sendSubscriberNotifiationsOn entries. Returning null for next notification time.",
);
return null;
}
for (const recurringItem of notificationSchedules) {
if (!recurringItem) {
continue;
}
const notificationDate: Date = Recurring.getNextDateInterval(
data.eventScheduledDate,
recurringItem,

View File

@@ -510,12 +510,30 @@ export class Service extends DatabaseService<ScheduledMaintenanceStateTimeline>
monitors: {
_id: true,
},
nextSubscriberNotificationBeforeTheEventAt: true,
},
props: {
isRoot: true,
},
});
const hasProgressedBeyondScheduledState: boolean = Boolean(
scheduledMaintenanceState && !scheduledMaintenanceState.isScheduledState,
);
if (
hasProgressedBeyondScheduledState &&
scheduledMaintenanceEvent?.nextSubscriberNotificationBeforeTheEventAt
) {
await ScheduledMaintenanceService.updateOneById({
id: createdItem.scheduledMaintenanceId!,
data: {
nextSubscriberNotificationBeforeTheEventAt: null,
},
props: onCreate.createBy.props,
});
}
if (isOngoingState) {
if (
scheduledMaintenanceEvent &&

View File

@@ -48,19 +48,26 @@ export class Service extends DatabaseService<StatusPageDomain> {
);
}
if (createBy.data.subdomain) {
// trim and lowercase the subdomain.
createBy.data.subdomain = createBy.data.subdomain.trim().toLowerCase();
let normalizedSubdomain: string =
createBy.data.subdomain?.trim().toLowerCase() || "";
if (normalizedSubdomain === "@") {
normalizedSubdomain = "";
}
createBy.data.subdomain = normalizedSubdomain;
if (domain) {
createBy.data.fullDomain = (
createBy.data.subdomain +
"." +
domain.domain?.toString()
)
.toLowerCase()
.trim();
const baseDomain: string =
domain.domain?.toString().toLowerCase().trim() || "";
if (!baseDomain) {
throw new BadDataException("Please select a valid domain.");
}
createBy.data.fullDomain = normalizedSubdomain
? `${normalizedSubdomain}.${baseDomain}`
: baseDomain;
}
createBy.data.cnameVerificationToken = ObjectID.generate().toString();

View File

@@ -5,7 +5,7 @@ import logger from "../Utils/Logger";
import DatabaseService from "./DatabaseService";
import MailService from "./MailService";
import StatusPageService from "./StatusPageService";
import { FileRoute } from "../../ServiceRoute";
import { StatusPageApiRoute } from "../../ServiceRoute";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import Hostname from "../../Types/API/Hostname";
import Protocol from "../../Types/API/Protocol";
@@ -106,6 +106,8 @@ export class Service extends DatabaseService<Model> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const statusPageIdString: string | null =
statusPage.id?.toString() || statusPage._id?.toString() || null;
MailService.sendMail(
{
@@ -115,12 +117,13 @@ export class Service extends DatabaseService<Model> {
vars: {
statusPageName: statusPageName!,
statusPageUrl: statusPageURL,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
logoUrl:
statusPage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
homeURL: statusPageURL,
tokenVerifyUrl: URL.fromString(statusPageURL)
.addRoute("/reset-password/" + token)

View File

@@ -0,0 +1,368 @@
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/StatusPagePrivateUserSession";
import ObjectID from "../../Types/ObjectID";
import { JSONObject } from "../../Types/JSON";
import HashedString from "../../Types/HashedString";
import { EncryptionSecret } from "../EnvironmentConfig";
import OneUptimeDate from "../../Types/Date";
import Text from "../../Types/Text";
import logger from "../Utils/Logger";
import Exception from "../../Types/Exception/Exception";
import BadDataException from "../../Types/Exception/BadDataException";
export interface SessionMetadata {
session: Model;
refreshToken: string;
refreshTokenExpiresAt: Date;
}
export interface CreateSessionOptions {
projectId: ObjectID;
statusPageId: ObjectID;
statusPagePrivateUserId: ObjectID;
refreshToken?: string | undefined;
refreshTokenExpiresAt?: Date | undefined;
ipAddress?: string | undefined;
userAgent?: string | undefined;
deviceName?: string | undefined;
deviceType?: string | undefined;
deviceOS?: string | undefined;
deviceBrowser?: string | undefined;
additionalInfo?: JSONObject | undefined;
}
export interface RenewSessionOptions {
session: Model;
refreshTokenExpiresAt?: Date | undefined;
ipAddress?: string | undefined;
userAgent?: string | undefined;
deviceName?: string | undefined;
deviceType?: string | undefined;
deviceOS?: string | undefined;
deviceBrowser?: string | undefined;
additionalInfo?: JSONObject | undefined;
}
export interface TouchSessionOptions {
ipAddress?: string | undefined;
userAgent?: string | undefined;
}
export interface RevokeSessionOptions {
reason?: string | undefined;
}
export class Service extends DatabaseService<Model> {
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
private static readonly SHORT_TEXT_LIMIT: number = 100;
public constructor() {
super(Model);
}
public async createSession(
options: CreateSessionOptions,
): Promise<SessionMetadata> {
const refreshToken: string =
options.refreshToken || Service.generateRefreshToken();
const refreshTokenExpiresAt: Date =
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
const session: Model = this.buildSessionModel(options, {
refreshToken,
refreshTokenExpiresAt,
});
try {
const createdSession: Model = await this.create({
data: session,
props: {
isRoot: true,
},
});
return {
session: createdSession,
refreshToken,
refreshTokenExpiresAt,
};
} catch (error) {
throw error as Exception;
}
}
public async findActiveSessionByRefreshToken(
refreshToken: string,
): Promise<Model | null> {
const hashedValue: string = await HashedString.hashValue(
refreshToken,
EncryptionSecret,
);
const session: Model | null = await this.findOneBy({
query: {
refreshToken: new HashedString(hashedValue, true),
isRevoked: false,
},
select: {
_id: true,
projectId: true,
statusPageId: true,
statusPagePrivateUserId: true,
refreshTokenExpiresAt: true,
lastActiveAt: true,
additionalInfo: true,
deviceName: true,
deviceType: true,
deviceOS: true,
deviceBrowser: true,
ipAddress: true,
userAgent: true,
isRevoked: true,
},
props: {
isRoot: true,
},
});
if (!session) {
return null;
}
if (
!session.refreshTokenExpiresAt ||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
) {
return null;
}
return session;
}
public async renewSessionWithNewRefreshToken(
options: RenewSessionOptions,
): Promise<SessionMetadata> {
const refreshToken: string = Service.generateRefreshToken();
const refreshTokenExpiresAt: Date =
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
const updatePayload: Partial<Model> = {
refreshToken: HashedString.fromString(refreshToken),
refreshTokenExpiresAt: refreshTokenExpiresAt,
lastActiveAt: OneUptimeDate.getCurrentDate(),
isRevoked: false,
};
const ipAddress: string | undefined = Text.truncate(
options.ipAddress,
Service.SHORT_TEXT_LIMIT,
);
if (ipAddress) {
updatePayload.ipAddress = ipAddress;
}
if (options.userAgent) {
updatePayload.userAgent = options.userAgent;
}
const deviceName: string | undefined = Text.truncate(
options.deviceName,
Service.SHORT_TEXT_LIMIT,
);
if (deviceName) {
updatePayload.deviceName = deviceName;
}
const deviceType: string | undefined = Text.truncate(
options.deviceType,
Service.SHORT_TEXT_LIMIT,
);
if (deviceType) {
updatePayload.deviceType = deviceType;
}
const deviceOS: string | undefined = Text.truncate(
options.deviceOS,
Service.SHORT_TEXT_LIMIT,
);
if (deviceOS) {
updatePayload.deviceOS = deviceOS;
}
const deviceBrowser: string | undefined = Text.truncate(
options.deviceBrowser,
Service.SHORT_TEXT_LIMIT,
);
if (deviceBrowser) {
updatePayload.deviceBrowser = deviceBrowser;
}
if (options.additionalInfo || options.session.additionalInfo) {
updatePayload.additionalInfo = {
...(options.session.additionalInfo || {}),
...(options.additionalInfo || {}),
} as JSONObject;
}
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
id: options.session.id!,
data: updatePayload as any,
props: {
isRoot: true,
},
});
if (!updatedSession) {
throw new BadDataException("Unable to renew status page user session");
}
return {
session: updatedSession,
refreshToken,
refreshTokenExpiresAt,
};
}
public async touchSession(
sessionId: ObjectID,
options: TouchSessionOptions,
): Promise<void> {
const updatePayload: Partial<Model> = {
lastActiveAt: OneUptimeDate.getCurrentDate(),
};
const ipAddress: string | undefined = Text.truncate(
options.ipAddress,
Service.SHORT_TEXT_LIMIT,
);
if (ipAddress) {
updatePayload.ipAddress = ipAddress;
}
if (options.userAgent) {
updatePayload.userAgent = options.userAgent;
}
try {
await this.updateOneById({
id: sessionId,
data: updatePayload as any,
props: {
isRoot: true,
},
});
} catch (err) {
logger.warn(
`Failed to update status page session activity for session ${sessionId.toString()}: ${(err as Error).message}`,
);
}
}
public async revokeSessionById(
sessionId: ObjectID,
options?: RevokeSessionOptions,
): Promise<void> {
await this.updateOneById({
id: sessionId,
data: {
isRevoked: true,
revokedAt: OneUptimeDate.getCurrentDate(),
revokedReason: options?.reason ?? null,
},
props: {
isRoot: true,
},
});
}
public async revokeSessionByRefreshToken(
refreshToken: string,
options?: RevokeSessionOptions,
): Promise<void> {
const session: Model | null =
await this.findActiveSessionByRefreshToken(refreshToken);
if (!session || !session.id) {
return;
}
await this.revokeSessionById(session.id, options);
}
private buildSessionModel(
options: CreateSessionOptions,
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
): Model {
const session: Model = new Model();
session.projectId = options.projectId;
session.statusPageId = options.statusPageId;
session.statusPagePrivateUserId = options.statusPagePrivateUserId;
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
session.lastActiveAt = OneUptimeDate.getCurrentDate();
if (options.userAgent) {
session.userAgent = options.userAgent;
}
const deviceName: string | undefined = Text.truncate(
options.deviceName,
Service.SHORT_TEXT_LIMIT,
);
if (deviceName) {
session.deviceName = deviceName;
}
const deviceType: string | undefined = Text.truncate(
options.deviceType,
Service.SHORT_TEXT_LIMIT,
);
if (deviceType) {
session.deviceType = deviceType;
}
const deviceOS: string | undefined = Text.truncate(
options.deviceOS,
Service.SHORT_TEXT_LIMIT,
);
if (deviceOS) {
session.deviceOS = deviceOS;
}
const deviceBrowser: string | undefined = Text.truncate(
options.deviceBrowser,
Service.SHORT_TEXT_LIMIT,
);
if (deviceBrowser) {
session.deviceBrowser = deviceBrowser;
}
const ipAddress: string | undefined = Text.truncate(
options.ipAddress,
Service.SHORT_TEXT_LIMIT,
);
if (ipAddress) {
session.ipAddress = ipAddress;
}
session.additionalInfo = {
...(options.additionalInfo || {}),
} as JSONObject;
return session;
}
private static generateRefreshToken(): string {
return ObjectID.generate().toString();
}
private static getRefreshTokenExpiry(): Date {
return OneUptimeDate.getSomeDaysAfter(
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
);
}
}
export default new Service();

View File

@@ -42,11 +42,12 @@ import StatusPageSubscriberService from "./StatusPageSubscriberService";
import StatusPageSubscriber from "../../Models/DatabaseModels/StatusPageSubscriber";
import MailService from "./MailService";
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
import { FileRoute } from "../../ServiceRoute";
import { StatusPageApiRoute } from "../../ServiceRoute";
import ProjectSMTPConfigService from "./ProjectSmtpConfigService";
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
import StatusPageResourceService from "./StatusPageResourceService";
import Dictionary from "../../Types/Dictionary";
import { JSONObject } from "../../Types/JSON";
import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
import MonitorGroupResourceService from "./MonitorGroupResourceService";
import QueryHelper from "../Types/Database/QueryHelper";
@@ -61,6 +62,11 @@ import IP from "../../Types/IP/IP";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import ForbiddenException from "../../Types/Exception/ForbiddenException";
import CommonAPI from "../API/CommonAPI";
import MasterPasswordRequiredException from "../../Types/Exception/MasterPasswordRequiredException";
import {
MASTER_PASSWORD_COOKIE_IDENTIFIER,
MASTER_PASSWORD_REQUIRED_MESSAGE,
} from "../../Types/StatusPage/MasterPassword";
export interface StatusPageReportItem {
resourceName: string;
@@ -389,6 +395,8 @@ export class Service extends DatabaseService<StatusPage> {
_id: true,
isPublicStatusPage: true,
ipWhitelist: true,
enableMasterPassword: true,
masterPassword: true,
},
});
@@ -462,6 +470,34 @@ export class Service extends DatabaseService<StatusPage> {
}
}
const shouldEnforceMasterPassword: boolean = Boolean(
statusPage &&
statusPage.enableMasterPassword &&
statusPage.masterPassword &&
!statusPage.isPublicStatusPage,
);
if (shouldEnforceMasterPassword) {
const hasValidMasterPassword: boolean =
this.hasValidMasterPasswordCookie({
req,
statusPageId,
});
if (hasValidMasterPassword) {
return {
hasReadAccess: true,
};
}
return {
hasReadAccess: false,
error: new MasterPasswordRequiredException(
MASTER_PASSWORD_REQUIRED_MESSAGE,
),
};
}
// if it does not have public access, check if this user has access.
const items: Array<StatusPage> = await this.findBy({
@@ -493,6 +529,33 @@ export class Service extends DatabaseService<StatusPage> {
};
}
private hasValidMasterPasswordCookie(data: {
req: ExpressRequest;
statusPageId: ObjectID;
}): boolean {
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
data.req,
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
);
if (!token) {
return false;
}
try {
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
return (
payload["statusPageId"] === data.statusPageId.toString() &&
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
);
} catch (err) {
logger.error(err);
}
return false;
}
@CaptureSpan()
public async getMonitorStatusTimelineForStatusPage(data: {
monitorIds: Array<ObjectID>;
@@ -750,6 +813,9 @@ export class Service extends DatabaseService<StatusPage> {
const statusPageName: string =
statuspage.pageTitle || statuspage.name || "Status Page";
const statusPageIdString: string | null =
statuspage.id?.toString() || statuspage._id?.toString() || null;
const report: StatusPageReport = await this.getReportByStatusPage({
statusPageId: statuspage.id!,
historyDays: statuspage.reportDataInDays || 14,
@@ -775,14 +841,16 @@ export class Service extends DatabaseService<StatusPage> {
subscriberEmailNotificationFooterText:
Service.getSubscriberEmailFooterText(statuspage),
statusPageUrl: statusPageURL,
detailsUrl: statusPageURL,
hasResources: report.totalResources > 0 ? "true" : "false",
report: report as any,
logoUrl: statuspage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statuspage.logoFileId)
.toString()
: "",
logoUrl:
statuspage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
isPublicStatusPage: statuspage.isPublicStatusPage
? "true"
: "false",
@@ -806,6 +874,7 @@ export class Service extends DatabaseService<StatusPage> {
if (data.email) {
// force send to this email instead of sending to all subscribers.
await sendEmail(data.email, null);
return; // don't notify subscribers when explicitly sending a test email.
}
const subscribers: Array<StatusPageSubscriber> =

View File

@@ -16,7 +16,7 @@ import ProjectCallSMSConfigService from "./ProjectCallSMSConfigService";
import ProjectService, { CurrentPlan } from "./ProjectService";
import SmsService from "./SmsService";
import StatusPageService from "./StatusPageService";
import { FileRoute } from "../../ServiceRoute";
import { StatusPageApiRoute } from "../../ServiceRoute";
import Hostname from "../../Types/API/Hostname";
import Protocol from "../../Types/API/Protocol";
import URL from "../../Types/API/URL";
@@ -522,6 +522,8 @@ Stay informed about service availability! 🚀`;
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
logger.debug(`HTTP Protocol: ${httpProtocol}`);
const statusPageIdString: string | null =
statusPage.id?.toString() || statusPage._id?.toString() || null;
const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink({
statusPageUrl: statusPageURL,
@@ -547,12 +549,13 @@ Stay informed about service availability! 🚀`;
templateType: EmailTemplateType.ConfirmStatusPageSubscription,
vars: {
statusPageName: statusPageName,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
logoUrl:
statusPage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
statusPageUrl: statusPageURL,
isPublicStatusPage: statusPage.isPublicStatusPage
? "true"
@@ -656,6 +659,8 @@ Stay informed about service availability! 🚀`;
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
logger.debug(`HTTP Protocol: ${httpProtocol}`);
const statusPageIdString: string | null =
statusPage.id?.toString() || statusPage._id?.toString() || null;
const unsubscribeLink: string = this.getUnsubscribeLink(
URL.fromString(statusPageURL),
@@ -675,12 +680,13 @@ Stay informed about service availability! 🚀`;
templateType: EmailTemplateType.SubscribedToStatusPage,
vars: {
statusPageName: statusPageName,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
logoUrl:
statusPage.logoFileId && statusPageIdString
? new URL(httpProtocol, host)
.addRoute(StatusPageApiRoute)
.addRoute(`/logo/${statusPageIdString}`)
.toString()
: "",
statusPageUrl: statusPageURL,
isPublicStatusPage: statusPage.isPublicStatusPage
? "true"

View File

@@ -44,10 +44,11 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
}
@CaptureSpan()
private async isSCIMEnabled(projectId: ObjectID): Promise<boolean> {
private async isSCIMPushGroupsEnabled(projectId: ObjectID): Promise<boolean> {
const count: PositiveNumber = await ProjectSCIMService.countBy({
query: {
projectId: projectId,
enablePushGroups: true,
},
props: {
isRoot: true,
@@ -63,12 +64,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
// Check if SCIM is enabled for the project
if (
!createBy.props.isRoot &&
(await this.isSCIMEnabled(
(await this.isSCIMPushGroupsEnabled(
createBy.data.projectId! || createBy.props.tenantId,
))
) {
throw new BadDataException(
"Cannot invite team members when SCIM is enabled for this project.",
"Cannot invite team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
);
}
@@ -311,10 +312,10 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
!deleteBy.props.isRoot &&
members.length > 0 &&
members[0]?.projectId &&
(await this.isSCIMEnabled(members[0].projectId))
(await this.isSCIMPushGroupsEnabled(members[0].projectId))
) {
throw new BadDataException(
"Cannot delete team members when SCIM is enabled for this project.",
"Cannot delete team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
);
}
@@ -345,7 +346,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
},
});
if (membersInTeam.toNumber() <= 1) {
// Skip the one-member guard when SCIM manages membership for the project.
const isPushGroupsManaged: boolean = await this.isSCIMPushGroupsEnabled(
member.projectId!,
);
if (!isPushGroupsManaged && membersInTeam.toNumber() <= 1) {
throw new BadDataException(
Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
);

Some files were not shown because too many files have changed in this diff Show More