mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
252 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30940991e0 | ||
|
|
a6d3047007 | ||
|
|
2ae0d139ea | ||
|
|
1507583cf3 | ||
|
|
310e1b764c | ||
|
|
1d7a25f2bb | ||
|
|
3bc89a8cae | ||
|
|
ff54b5a26c | ||
|
|
07b9f38e90 | ||
|
|
f612940668 | ||
|
|
6046bda443 | ||
|
|
cd97a72ef2 | ||
|
|
c20fbcc7a4 | ||
|
|
f22e410860 | ||
|
|
3059ca848e | ||
|
|
ce8b8c3b58 | ||
|
|
78846a3f95 | ||
|
|
994f329a49 | ||
|
|
d621df58cd | ||
|
|
0e0ccd9651 | ||
|
|
cc0adea216 | ||
|
|
8ec0825a52 | ||
|
|
4d083f9663 | ||
|
|
81b3795b1c | ||
|
|
74cf9ae184 | ||
|
|
9ba4c0bfdd | ||
|
|
85e83c6822 | ||
|
|
65eedde511 | ||
|
|
7d45056ae4 | ||
|
|
f2db382087 | ||
|
|
3e99783119 | ||
|
|
de6bbbee8c | ||
|
|
0309c2d7e8 | ||
|
|
1c9bb0605b | ||
|
|
b8b98be7a0 | ||
|
|
6c037b0996 | ||
|
|
326b60c260 | ||
|
|
e13ab0b214 | ||
|
|
01f8a27dd2 | ||
|
|
081029b49a | ||
|
|
32d55fdc46 | ||
|
|
69aa680dd1 | ||
|
|
117c02d457 | ||
|
|
f74fcb3734 | ||
|
|
676d6598d7 | ||
|
|
af1edfef99 | ||
|
|
56cadf01fd | ||
|
|
6a0ef8d940 | ||
|
|
1e01942218 | ||
|
|
5dab4f8042 | ||
|
|
3e4a50d430 | ||
|
|
96d236b034 | ||
|
|
ade5e69aa0 | ||
|
|
4733c710b2 | ||
|
|
1c8922249e | ||
|
|
73c787836f | ||
|
|
ec6f3d84d7 | ||
|
|
1f2df5f3ee | ||
|
|
121a78ea8d | ||
|
|
832ab4ab24 | ||
|
|
445f3906ee | ||
|
|
c869acc0e5 | ||
|
|
0d2b2a272b | ||
|
|
d8ac1c39b7 | ||
|
|
e31dbe935c | ||
|
|
35e46cebfc | ||
|
|
891861f396 | ||
|
|
fbc38230b8 | ||
|
|
61561f9745 | ||
|
|
13e5f57160 | ||
|
|
7600085473 | ||
|
|
99641d6994 | ||
|
|
dea66cc8d8 | ||
|
|
1dd43a69a0 | ||
|
|
e539cb7ae3 | ||
|
|
5269f7a164 | ||
|
|
8e1d6b420f | ||
|
|
311f7dbb5b | ||
|
|
9ac64b5873 | ||
|
|
e54126e6bf | ||
|
|
a4acc59505 | ||
|
|
854bc297a6 | ||
|
|
69bfb48573 | ||
|
|
9ef7f720b1 | ||
|
|
2155dcad65 | ||
|
|
fb4da29ade | ||
|
|
d146d33059 | ||
|
|
978ac9155f | ||
|
|
cf9b4f0eba | ||
|
|
d26a2aa995 | ||
|
|
c8301e21eb | ||
|
|
1a24232a1c | ||
|
|
69c94835fe | ||
|
|
706a9145ac | ||
|
|
9bde6194a5 | ||
|
|
3dd8c62bc4 | ||
|
|
895a434d0e | ||
|
|
d3ec3a60df | ||
|
|
df15a2dcae | ||
|
|
5e4b24dcfb | ||
|
|
0eae73c4e5 | ||
|
|
deb3f81e5d | ||
|
|
dcb3fe6f69 | ||
|
|
4bbecb3013 | ||
|
|
52954f3702 | ||
|
|
12a7fff668 | ||
|
|
14e489a719 | ||
|
|
83f2935f41 | ||
|
|
28855d482e | ||
|
|
7c11ebdacf | ||
|
|
592c806465 | ||
|
|
7a3d0266c4 | ||
|
|
e9b7368cf1 | ||
|
|
f8c0004f85 | ||
|
|
b007cb8bbd | ||
|
|
541015766c | ||
|
|
5310087287 | ||
|
|
7649c6c566 | ||
|
|
1b2650f6df | ||
|
|
de3586d60e | ||
|
|
a0ca579c2f | ||
|
|
9bc7a115a1 | ||
|
|
aef7af0a9a | ||
|
|
e9f2e46e16 | ||
|
|
56fd18f7c9 | ||
|
|
ecccd8b536 | ||
|
|
dd2f0f37f2 | ||
|
|
600a5eafe3 | ||
|
|
9b07bf7a08 | ||
|
|
d341f6c2b0 | ||
|
|
06a030f518 | ||
|
|
b02fed6e5b | ||
|
|
310884bd73 | ||
|
|
0c625d52c2 | ||
|
|
82ab70f396 | ||
|
|
24d9c9dbc0 | ||
|
|
d9f1dc9fd2 | ||
|
|
4b503471bd | ||
|
|
b523434be3 | ||
|
|
f32b1950d9 | ||
|
|
e1c45a5c99 | ||
|
|
195655b4df | ||
|
|
9d7d65f0ef | ||
|
|
985b5410f6 | ||
|
|
d1dd57deec | ||
|
|
2ec6902537 | ||
|
|
cd130bc8ef | ||
|
|
3fcd1f694e | ||
|
|
b98b43b9f6 | ||
|
|
b59c76f771 | ||
|
|
4d2e386328 | ||
|
|
deddcbe152 | ||
|
|
e305284fe2 | ||
|
|
3163debdb8 | ||
|
|
f827237a80 | ||
|
|
c038e39620 | ||
|
|
b8c1190c9f | ||
|
|
7f5ff5068e | ||
|
|
943acc8567 | ||
|
|
81798211ea | ||
|
|
39596f6a42 | ||
|
|
36a181e77e | ||
|
|
032c03a877 | ||
|
|
500299fb2f | ||
|
|
b959e84032 | ||
|
|
30edc194f4 | ||
|
|
0a20894dd1 | ||
|
|
1045a7399f | ||
|
|
0f3ef0027b | ||
|
|
2f43bc5c65 | ||
|
|
8d9f7e125d | ||
|
|
3ac841ddc1 | ||
|
|
9fc8c6f7a2 | ||
|
|
62cd974235 | ||
|
|
a0d03238ee | ||
|
|
4303cf00cc | ||
|
|
747ea70de5 | ||
|
|
6dd4ef22df | ||
|
|
4b97c79ae2 | ||
|
|
d34b118c68 | ||
|
|
a3ff2e1067 | ||
|
|
e6ef2a7945 | ||
|
|
9bf8d5d941 | ||
|
|
f3ee93bd48 | ||
|
|
5b78fee225 | ||
|
|
dd73947b7f | ||
|
|
78998fb123 | ||
|
|
0bcfccffe0 | ||
|
|
3ab45f40ca | ||
|
|
bb2f610bc8 | ||
|
|
9b685133c4 | ||
|
|
33c4943794 | ||
|
|
a7f8aa4faa | ||
|
|
722fe30c8f | ||
|
|
500104eb81 | ||
|
|
41c3a14dfa | ||
|
|
113eda94fa | ||
|
|
c6ce43f7cc | ||
|
|
6d5bc60127 | ||
|
|
b7b7b28834 | ||
|
|
5a0b0d7c61 | ||
|
|
79bf7ce7ee | ||
|
|
4ab150bf75 | ||
|
|
951668c982 | ||
|
|
d7845407f0 | ||
|
|
8c5e3187ab | ||
|
|
d2ae1cd845 | ||
|
|
8cb64fbe66 | ||
|
|
092b858873 | ||
|
|
81d19722f6 | ||
|
|
1f7b268875 | ||
|
|
38b32a6090 | ||
|
|
373159cb29 | ||
|
|
ec86ef4c0e | ||
|
|
270231374b | ||
|
|
aac4281602 | ||
|
|
bdea1139a4 | ||
|
|
14fdfa6d17 | ||
|
|
3b22747dbf | ||
|
|
eb9e20dad5 | ||
|
|
5aa4b883ad | ||
|
|
ea38e2621f | ||
|
|
b2cb95e1fc | ||
|
|
b174b9795a | ||
|
|
9d1caa8336 | ||
|
|
52e8669960 | ||
|
|
f0505725a7 | ||
|
|
7897641ef7 | ||
|
|
761f5f35e9 | ||
|
|
241586ff4a | ||
|
|
f8fc1a9dae | ||
|
|
6bbcc0a301 | ||
|
|
0e6604aa11 | ||
|
|
ade84a23ff | ||
|
|
6ff883b54e | ||
|
|
3546d92143 | ||
|
|
e932eb1b1d | ||
|
|
cbca712af8 | ||
|
|
8490128833 | ||
|
|
b80e126540 | ||
|
|
5494a2244e | ||
|
|
23be5b1736 | ||
|
|
9f3a9bc915 | ||
|
|
84d322f476 | ||
|
|
f94fbcc2ae | ||
|
|
0e85162b50 | ||
|
|
a5927f3681 | ||
|
|
0d37587199 | ||
|
|
4674578c90 | ||
|
|
87d280edbd | ||
|
|
d44ddd6781 | ||
|
|
7271481fb7 |
1
.github/workflows/compile.yml
vendored
1
.github/workflows/compile.yml
vendored
@@ -189,6 +189,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: sudo apt-get update
|
||||
- run: cd Common && npm install
|
||||
- run: cd E2E && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
6
.github/workflows/test-release.yaml
vendored
6
.github/workflows/test-release.yaml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Push Test Images to Docker Hub and GitHub Container Registry
|
||||
|
||||
concurrency:
|
||||
group: test-release
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -1526,7 +1530,7 @@ jobs:
|
||||
|
||||
test-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [llm-docker-image-deploy, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, probe-docker-image-deploy, haraka-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [infrastructure-agent-deploy, llm-docker-image-deploy, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, probe-docker-image-deploy, haraka-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
|
||||
58
.github/workflows/test.e2e.yaml
vendored
58
.github/workflows/test.e2e.yaml
vendored
@@ -1,58 +0,0 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'hotfix-*' # excludes hotfix branches
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: npm run prerun && bash ./Tests/Scripts/enable-billing-env-var.sh
|
||||
- run: npm run dev
|
||||
- name: Wait for server to start
|
||||
run: bash ./Tests/Scripts/status-check.sh http://localhost
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
run: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
# Run this on failure
|
||||
if: failure()
|
||||
with:
|
||||
# Name of the artifact to upload.
|
||||
# Optional. Default is 'artifact'
|
||||
name: test-results
|
||||
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
# Required.
|
||||
path: |
|
||||
./E2E
|
||||
|
||||
|
||||
# Duration after which artifact will expire in days. 0 means using default retention.
|
||||
# Minimum 1 day.
|
||||
# Maximum 90 days unless changed from the repository settings page.
|
||||
# Optional. Defaults to repository settings.
|
||||
retention-days: 7
|
||||
22
.vscode/launch.json
vendored
22
.vscode/launch.json
vendored
@@ -93,7 +93,7 @@
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/Worker",
|
||||
"localRoot": "${workspaceFolder}/Workflow",
|
||||
"name": "Workflow: Debug with Docker",
|
||||
"port": 8735,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/Worker",
|
||||
"localRoot": "${workspaceFolder}/Docs",
|
||||
"name": "Docs: Debug with Docker",
|
||||
"port": 8738,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/Worker",
|
||||
"localRoot": "${workspaceFolder}/APIReference",
|
||||
"name": "API Reference: Debug with Docker",
|
||||
"port": 8737,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
@@ -151,7 +151,7 @@
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/Probe",
|
||||
"name": "Probe: Debug with Docker",
|
||||
"port": 9655,
|
||||
"port": 9229,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
@@ -259,20 +259,6 @@
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/Workers",
|
||||
"name": "Workers: Debug with Docker",
|
||||
"port": 9654,
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"restart": true,
|
||||
"autoAttachChildProcesses": true
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"localRoot": "${workspaceFolder}/StatusPage",
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
</h2>
|
||||
|
||||
<script>
|
||||
function showPermissions(id){
|
||||
var permissionsblock = document.getElementById(id+"-permissions");
|
||||
var viewPermissionsBtn = document.getElementById(id+"-view-permissions");
|
||||
function showPermissions(id) {
|
||||
var permissionsblock = document.getElementById(id + "-permissions");
|
||||
var viewPermissionsBtn = document.getElementById(id + "-view-permissions");
|
||||
|
||||
if(permissionsblock.style.display === "none"){
|
||||
if (permissionsblock.style.display === "none") {
|
||||
permissionsblock.style.display = "block";
|
||||
viewPermissionsBtn.innerHTML = "Hide Permissions";
|
||||
}else{
|
||||
} else {
|
||||
permissionsblock.style.display = "none";
|
||||
viewPermissionsBtn.innerHTML = "View Permissions";
|
||||
}
|
||||
@@ -48,11 +48,16 @@
|
||||
<dt class="sr-only">Description</dt>
|
||||
<dd class="w-full flex-none text-sm [&>:first-child]:mt-0 [&>:last-child]:mb-0">
|
||||
<p>
|
||||
<%= pageData.columns[Object.keys(pageData.columns)[i]].description -%> <a class="text-gray-500 hover:underline cursor-pointer text-xs" id="<%= Object.keys(pageData.columns)[i] -%>-view-permissions" onclick="showPermissions('<%= Object.keys(pageData.columns)[i] -%>')">View Permissions</a>
|
||||
<%= pageData.columns[Object.keys(pageData.columns)[i]].description -%> <a
|
||||
class="text-gray-500 hover:underline cursor-pointer text-xs"
|
||||
id="<%= Object.keys(pageData.columns)[i] -%>-view-permissions"
|
||||
onclick="showPermissions('<%= Object.keys(pageData.columns)[i] -%>')">View
|
||||
Permissions</a>
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dd class="font-mono text-xs" style="display: none;" id="<%= Object.keys(pageData.columns)[i] -%>-permissions">
|
||||
<dd class="font-mono text-xs" style="display: none;"
|
||||
id="<%= Object.keys(pageData.columns)[i] -%>-permissions">
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<span class="text-gray-700 text-xs">Permissions to Create: </span>
|
||||
@@ -319,8 +324,7 @@
|
||||
|
||||
<div class="[&>:first-child]:mt-0 [&>:last-child]:mb-0 xl:sticky xl:top-24">
|
||||
<%- include('../partials/code', {title: "Example Item Request" , requestUrl:
|
||||
pageData.apiPath+"/3599ee69-43a7-42d7/get-item", code: pageData.itemRequest, requestType: "POST" })
|
||||
-%>
|
||||
pageData.apiPath+"/:id/get-item", code: pageData.itemRequest, requestType: "POST" }) -%>
|
||||
<%- include('../partials/code', {title: "Example Item Response" , code: pageData.itemResponse,
|
||||
requestType: "" }) -%>
|
||||
</div>
|
||||
@@ -469,11 +473,34 @@
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="border border-gray-100 bg-gray-50 rounded-md p-4 text-sm mt-10">
|
||||
<h4 class="font-semibold text-gray-700 ">For clients that do not support PUT requests<h4>
|
||||
<p class="text-gray-500 text-xs mt-4">
|
||||
You can also update an object by sending a POST or GET request to these endpoints with the
|
||||
same
|
||||
request headers and body.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-x-3 mt-10"><span
|
||||
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-sky-300 bg-sky-400/10 text-sky-500 ">POST</span><span
|
||||
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
|
||||
class="font-mono text-xs text-zinc-400">
|
||||
<%= pageData.apiPath -%>/:id/update-item
|
||||
</span></div>
|
||||
<div class="flex items-center gap-x-3 mt-10"><span
|
||||
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-emerald-300 bg-emerald-400/10 text-emerald-500 ">GET</span><span
|
||||
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
|
||||
class="font-mono text-xs text-zinc-400">
|
||||
<%= pageData.apiPath -%>/:id/update-item
|
||||
</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="[&>:first-child]:mt-0 [&>:last-child]:mb-0 xl:sticky xl:top-24">
|
||||
<%- include('../partials/code', {title: "Example Update Request" , requestUrl:
|
||||
pageData.apiPath+"/3599ee69-43a7-42d7", code: pageData.updateRequest, requestType: "PUT" }) -%>
|
||||
pageData.apiPath+"/:id", code: pageData.updateRequest, requestType: "PUT" }) -%>
|
||||
<%- include('../partials/code', {title: "Example Update Response" , code: pageData.updateResponse,
|
||||
requestType: "" }) -%>
|
||||
</div>
|
||||
@@ -498,11 +525,34 @@
|
||||
<p>This endpoint allows you to delete object by its ID. </p>
|
||||
|
||||
|
||||
<div class="border border-gray-100 bg-gray-50 rounded-md p-4 text-sm mt-10">
|
||||
<h4 class="font-semibold text-gray-700 ">For clients that do not support DELETE requests<h4>
|
||||
<p class="text-gray-500 text-xs mt-4">
|
||||
You can also delete an object by sending a POST or GET request to these endpoints with the
|
||||
same
|
||||
request headers and body.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-x-3 mt-10"><span
|
||||
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-sky-300 bg-sky-400/10 text-sky-500 ">POST</span><span
|
||||
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
|
||||
class="font-mono text-xs text-zinc-400">
|
||||
<%= pageData.apiPath -%>/:id/delete-item
|
||||
</span></div>
|
||||
<div class="flex items-center gap-x-3 mt-10"><span
|
||||
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-emerald-300 bg-emerald-400/10 text-emerald-500 ">GET</span><span
|
||||
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
|
||||
class="font-mono text-xs text-zinc-400">
|
||||
<%= pageData.apiPath -%>/:id/delete-item
|
||||
</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="[&>:first-child]:mt-0 [&>:last-child]:mb-0 xl:sticky xl:top-24">
|
||||
<%- include('../partials/code', {title: "Example Delete Request" , requestUrl:
|
||||
pageData.apiPath+"/3599ee69-43a7-42d7", code: pageData.deleteRequest, requestType: "DELETE" }) -%>
|
||||
pageData.apiPath+"/:id", code: pageData.deleteRequest, requestType: "DELETE" }) -%>
|
||||
<%- include('../partials/code', {title: "Example Delete Response" , code: pageData.deleteResponse,
|
||||
requestType: "" }) -%>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ const DashboardProjectPicker: FunctionComponent<ComponentProps> = (
|
||||
minLength: 6,
|
||||
},
|
||||
footerElement: getFooter(),
|
||||
fieldType: FormFieldSchemaType.RadioButton,
|
||||
fieldType: FormFieldSchemaType.OptionChooserButton,
|
||||
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
|
||||
getAllEnvVars(),
|
||||
).map((plan: SubscriptionPlan): RadioButton => {
|
||||
|
||||
@@ -75,7 +75,7 @@ const Projects: FunctionComponent = (): ReactElement => {
|
||||
minLength: 6,
|
||||
},
|
||||
footerElement: getFooter(),
|
||||
fieldType: FormFieldSchemaType.RadioButton,
|
||||
fieldType: FormFieldSchemaType.OptionChooserButton,
|
||||
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
|
||||
getAllEnvVars(),
|
||||
).map((plan: SubscriptionPlan): RadioButton => {
|
||||
|
||||
@@ -512,6 +512,29 @@ import ScheduledMaintenanceFeedService, {
|
||||
Service as ScheduledMaintenanceFeedServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceFeedService";
|
||||
|
||||
import SlackAPI from "Common/Server/API/SlackAPI";
|
||||
|
||||
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService, {
|
||||
Service as WorkspaceProjectAuthTokenServiceType,
|
||||
} from "Common/Server/Services/WorkspaceProjectAuthTokenService";
|
||||
|
||||
import WorkspaceUserAuthToken from "Common/Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
|
||||
import WorkspaceUserAuthTokenService, {
|
||||
Service as WorkspaceUserAuthTokenServiceType,
|
||||
} from "Common/Server/Services/WorkspaceUserAuthTokenService";
|
||||
|
||||
import WorkspaceSetting from "Common/Models/DatabaseModels/WorkspaceSetting";
|
||||
import WorkspaceSettingService, {
|
||||
Service as WorkspaceSettingServiceType,
|
||||
} from "Common/Server/Services/WorkspaceSettingService";
|
||||
|
||||
import WorkspaceNotificationRule from "Common/Models/DatabaseModels/WorkspaceNotificationRule";
|
||||
import WorkspaceNotificationRuleService, {
|
||||
Service as WorkspaceNotificationRuleServiceType,
|
||||
} from "Common/Server/Services/WorkspaceNotificationRuleService";
|
||||
|
||||
const BaseAPIFeatureSet: FeatureSet = {
|
||||
init: async (): Promise<void> => {
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
@@ -534,6 +557,19 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// notification rule
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
WorkspaceNotificationRule,
|
||||
WorkspaceNotificationRuleServiceType
|
||||
>(
|
||||
WorkspaceNotificationRule,
|
||||
WorkspaceNotificationRuleService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<MonitorTest, MonitorTestServiceType>(
|
||||
@@ -542,6 +578,15 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
//service provider setting
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<WorkspaceSetting, WorkspaceSettingServiceType>(
|
||||
WorkspaceSetting,
|
||||
WorkspaceSettingService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentFeed, IncidentFeedServiceType>(
|
||||
@@ -574,6 +619,26 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
WorkspaceProjectAuthToken,
|
||||
WorkspaceProjectAuthTokenServiceType
|
||||
>(
|
||||
WorkspaceProjectAuthToken,
|
||||
WorkspaceProjectAuthTokenService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
// user auth token
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<WorkspaceUserAuthToken, WorkspaceUserAuthTokenServiceType>(
|
||||
WorkspaceUserAuthToken,
|
||||
WorkspaceUserAuthTokenService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<Alert, AlertServiceType>(Alert, AlertService).getRouter(),
|
||||
@@ -1368,6 +1433,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new ResellerPlanAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new GlobalConfigAPI().getRouter(),
|
||||
|
||||
@@ -36,6 +36,7 @@ import ProjectSSO from "Common/Models/DatabaseModels/ProjectSso";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import xml2js from "xml2js";
|
||||
import Name from "Common/Types/Name";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
@@ -283,6 +284,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
|
||||
let issuerUrl: string = "";
|
||||
let email: Email | null = null;
|
||||
let fullName: Name | null = null;
|
||||
|
||||
if (!req.params["projectId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
@@ -372,6 +374,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
|
||||
issuerUrl = SSOUtil.getIssuer(response);
|
||||
email = SSOUtil.getEmail(response);
|
||||
fullName = SSOUtil.getUserFullName(response);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Exception) {
|
||||
return Response.sendErrorResponse(req, res, err);
|
||||
@@ -420,6 +423,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
|
||||
alreadySavedUser = await UserService.createByEmail({
|
||||
email,
|
||||
name: fullName || undefined,
|
||||
isEmailVerified: true,
|
||||
generateRandomPassword: true,
|
||||
props: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import logger from "Common/Server/Utils/Logger";
|
||||
import xmlCrypto, { FileKeyInfo } from "xml-crypto";
|
||||
import xmldom from "xmldom";
|
||||
import zlib from "zlib";
|
||||
import Name from "Common/Types/Name";
|
||||
|
||||
export default class SSOUtil {
|
||||
public static createSAMLRequestUrl(data: {
|
||||
@@ -138,6 +139,88 @@ export default class SSOUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static getUserFullName(payload: JSONObject): Name | null {
|
||||
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
payload =
|
||||
(payload["saml2p:Response"] as JSONObject) ||
|
||||
(payload["samlp:Response"] as JSONObject) ||
|
||||
(payload["Response"] as JSONObject);
|
||||
|
||||
const samlAssertion: JSONArray =
|
||||
(payload["saml2:Assertion"] as JSONArray) ||
|
||||
(payload["saml:Assertion"] as JSONArray) ||
|
||||
(payload["Assertion"] as JSONArray);
|
||||
|
||||
if (!samlAssertion || samlAssertion.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const samlAttributeStatement: JSONArray =
|
||||
((samlAssertion[0] as JSONObject)[
|
||||
"saml2:AttributeStatement"
|
||||
] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)[
|
||||
"saml:AttributeStatement"
|
||||
] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)["AttributeStatement"] as JSONArray);
|
||||
|
||||
if (!samlAttributeStatement || samlAttributeStatement.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const samlAttribute: JSONArray =
|
||||
((samlAttributeStatement[0] as JSONObject)[
|
||||
"saml2:Attribute"
|
||||
] as JSONArray) ||
|
||||
((samlAttributeStatement[0] as JSONObject)[
|
||||
"saml:Attribute"
|
||||
] as JSONArray) ||
|
||||
((samlAttributeStatement[0] as JSONObject)["Attribute"] as JSONArray);
|
||||
|
||||
if (!samlAttribute || samlAttribute.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get displayName attribute.
|
||||
// {
|
||||
// "$": {
|
||||
// "Name": "http://schemas.microsoft.com/identity/claims/displayname"
|
||||
// },
|
||||
// "AttributeValue": [
|
||||
// "Nawaz Dhandala"
|
||||
// ]
|
||||
// },
|
||||
|
||||
for (let i: number = 0; i < samlAttribute.length; i++) {
|
||||
const attribute: JSONObject = samlAttribute[i] as JSONObject;
|
||||
if (
|
||||
attribute["$"] &&
|
||||
(attribute["$"] as JSONObject)["Name"]?.toString()
|
||||
) {
|
||||
const name: string | undefined = (attribute["$"] as JSONObject)[
|
||||
"Name"
|
||||
]?.toString();
|
||||
if (
|
||||
name &&
|
||||
name === "http://schemas.microsoft.com/identity/claims/displayname" &&
|
||||
attribute["AttributeValue"] &&
|
||||
Array.isArray(attribute["AttributeValue"]) &&
|
||||
attribute["AttributeValue"].length > 0
|
||||
) {
|
||||
const fullName: Name = new Name(
|
||||
attribute["AttributeValue"][0]!.toString() as string,
|
||||
);
|
||||
return fullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static getEmail(payload: JSONObject): Email {
|
||||
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
|
||||
throw new BadRequestException("SAML Response not found.");
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{{> InfoBlock info="No resources have been added to this status page yet."}}
|
||||
{{/ifCond}}
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{{> DetailBoxField title="" text=announcementDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{> DetailBoxField title="" text=eventDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
|
||||
1778
Clickhouse/config.xml
Normal file
1778
Clickhouse/config.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ import {
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import { TelemetryQuery } from "../../Types/Telemetry/TelemetryQuery";
|
||||
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
|
||||
|
||||
@EnableDocumentation()
|
||||
@AccessControlColumn("labels")
|
||||
@@ -1006,4 +1007,51 @@ export default class Alert extends BaseModel {
|
||||
nullable: true,
|
||||
})
|
||||
public telemetryQuery?: TelemetryQuery = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateAlert,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadAlert,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.Number,
|
||||
title: "Alert Number",
|
||||
description: "Alert Number",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: true,
|
||||
})
|
||||
public alertNumber?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.JSON,
|
||||
title: "Post Updates To Workspace Channel Name",
|
||||
description: "Post Updates To Workspace Channel Name",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import { TelemetryQuery } from "../../Types/Telemetry/TelemetryQuery";
|
||||
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
|
||||
|
||||
@EnableDocumentation()
|
||||
@AccessControlColumn("labels")
|
||||
@@ -1120,4 +1121,22 @@ export default class Incident extends BaseModel {
|
||||
nullable: true,
|
||||
})
|
||||
public incidentNumber?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.JSON,
|
||||
title: "Post Updates To Workspace Channel Name",
|
||||
description: "Post Updates To Workspace Channel Name",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
|
||||
}
|
||||
|
||||
@@ -161,10 +161,17 @@ import Dashboard from "./Dashboard";
|
||||
import MonitorTest from "./MonitorTest";
|
||||
import ScheduledMaintenanceFeed from "./ScheduledMaintenanceFeed";
|
||||
|
||||
import WorkspaceUserAuthToken from "./WorkspaceUserAuthToken";
|
||||
import WorkspaceProjectAuthToken from "./WorkspaceProjectAuthToken";
|
||||
import WorkspaceSetting from "./WorkspaceSetting";
|
||||
import WorkspaceNotificationRule from "./WorkspaceNotificationRule";
|
||||
|
||||
const AllModelTypes: Array<{
|
||||
new (): BaseModel;
|
||||
}> = [
|
||||
User,
|
||||
WorkspaceUserAuthToken,
|
||||
WorkspaceProjectAuthToken,
|
||||
Probe,
|
||||
Project,
|
||||
EmailVerificationToken,
|
||||
@@ -343,6 +350,9 @@ const AllModelTypes: Array<{
|
||||
Dashboard,
|
||||
|
||||
MonitorTest,
|
||||
|
||||
WorkspaceSetting,
|
||||
WorkspaceNotificationRule,
|
||||
];
|
||||
|
||||
const modelTypeMap: { [key: string]: { new (): BaseModel } } = {};
|
||||
|
||||
@@ -442,6 +442,7 @@ export default class MonitorTest extends BaseModel {
|
||||
})
|
||||
public monitorStepProbeResponse?: MonitorStepProbeResponse = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
import Recurring from "../../Types/Events/Recurring";
|
||||
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
|
||||
|
||||
@EnableDocumentation()
|
||||
@AccessControlColumn("labels")
|
||||
@@ -950,4 +951,51 @@ export default class ScheduledMaintenance extends BaseModel {
|
||||
nullable: true,
|
||||
})
|
||||
public nextSubscriberNotificationBeforeTheEventAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectScheduledMaintenance,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectScheduledMaintenance,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.Number,
|
||||
title: "Scheduled Maintenance Number",
|
||||
description: "Scheduled Maintenance Number",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: true,
|
||||
})
|
||||
public scheduledMaintenanceNumber?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.JSON,
|
||||
title: "Post Updates To Workspace Channel Name",
|
||||
description: "Post Updates To Workspace Channel Name",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
|
||||
}
|
||||
|
||||
@@ -1973,4 +1973,37 @@ export default class StatusPage extends BaseModel {
|
||||
default: UptimePrecision.TWO_DECIMAL,
|
||||
})
|
||||
public overallUptimePercentPrecision?: UptimePrecision = 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({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Subscriber Email Notification Footer Text",
|
||||
description: "Text to send to subscribers in the footer of the email.",
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public subscriberEmailNotificationFooterText?: string = undefined;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,12 @@ export default class StatusPageAnnouncement extends BaseModel {
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPageAnnouncement,
|
||||
],
|
||||
update: [],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditStatusPageAnnouncement,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
|
||||
461
Common/Models/DatabaseModels/WorkspaceNotificationRule.ts
Normal file
461
Common/Models/DatabaseModels/WorkspaceNotificationRule.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
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 TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import BaseNotificationRule from "../../Types/Workspace/NotificationRules/BaseNotificationRule";
|
||||
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
|
||||
import Permission from "../../Types/Permission";
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.DeleteWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/workspace-notification-rule"))
|
||||
@Entity({
|
||||
name: "WorkspaceNotificationRule",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WorkspaceNotificationRule",
|
||||
singularName: "Workspace Notification Rule",
|
||||
pluralName: "Workspace Notification Rules",
|
||||
icon: IconProp.Logs,
|
||||
tableDescription: "Notification Rule for Third Party Workspaces",
|
||||
})
|
||||
class WorkspaceNotificationRule extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Rule Name",
|
||||
description: "Name of the Notification Rule",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public name?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Rule Description",
|
||||
description: "Description of the Notification Rule",
|
||||
required: false,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Notification Rules",
|
||||
description: "Notification Rules for the Workspace",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.JSON,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public notificationRule?: BaseNotificationRule = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Event Type",
|
||||
description:
|
||||
"Event Type for the Workspace like Incident Created, Monitor Status Updated, etc.",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public eventType?: NotificationRuleEventType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Type",
|
||||
description: "Type of Workspace - slack, microsoft teams etc.",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceType?: WorkspaceType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
// deleted by userId
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateWorkspaceNotificationRule,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkspaceNotificationRule,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditWorkspaceNotificationRule,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default WorkspaceNotificationRule;
|
||||
379
Common/Models/DatabaseModels/WorkspaceProjectAuthToken.ts
Normal file
379
Common/Models/DatabaseModels/WorkspaceProjectAuthToken.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
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 TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import Permission from "../../Types/Permission";
|
||||
|
||||
export interface MiscData {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface SlackMiscData extends MiscData {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
botUserId: string;
|
||||
}
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
delete: [Permission.ProjectOwner, Permission.ProjectAdmin],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/workspace-project-auth-token"))
|
||||
@Entity({
|
||||
name: "WorkspaceProjectAuthToken",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WorkspaceProjectAuthToken",
|
||||
singularName: "Workspace Project Auth Token",
|
||||
pluralName: "Workspace Project Auth Tokens",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription: "Third Party Auth Token for the Project",
|
||||
})
|
||||
class WorkspaceProjectAuthToken extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Auth Token",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public authToken?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Type",
|
||||
description: "Type of Workspace - slack, microsoft teams etc.",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceType?: WorkspaceType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Project ID in Workspace",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceProjectId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Misc Data",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.JSON,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public miscData?: MiscData = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default WorkspaceProjectAuthToken;
|
||||
230
Common/Models/DatabaseModels/WorkspaceSetting.ts
Normal file
230
Common/Models/DatabaseModels/WorkspaceSetting.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
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 TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
|
||||
export interface Settings {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface SlackSettings extends Settings {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
botUserId: string;
|
||||
}
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/workspace-setting"))
|
||||
@Entity({
|
||||
name: "WorkspaceSetting",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WorkspaceSetting",
|
||||
singularName: "Workspace Setting",
|
||||
pluralName: "Workspace Settings",
|
||||
icon: IconProp.Settings,
|
||||
tableDescription: "Settings for Third Party Workspaces",
|
||||
})
|
||||
class WorkspaceSetting extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@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,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Settings",
|
||||
description: "Settings for the Workspace",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.JSON,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public settings?: SlackSettings = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Type",
|
||||
description: "Type of Workspace - slack, microsoft teams etc.",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceType?: WorkspaceType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
// deleted by userId
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default WorkspaceSetting;
|
||||
312
Common/Models/DatabaseModels/WorkspaceUserAuthToken.ts
Normal file
312
Common/Models/DatabaseModels/WorkspaceUserAuthToken.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
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 TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
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 WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
|
||||
export interface MiscData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SlackMiscData extends MiscData {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/workspace-user-auth-token"))
|
||||
@Entity({
|
||||
name: "WorkspaceUserAuthToken",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WorkspaceUserAuthToken",
|
||||
singularName: "Workspace User Auth Token",
|
||||
pluralName: "Workspace User Auth Tokens",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription: "Third Party Auth Token for the User",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class WorkspaceUserAuthToken extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Auth Token",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public authToken?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "User ID in Service",
|
||||
description: "User ID in the Workspace",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceUserId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Workspace Type",
|
||||
description: "Type of Workspace - slack, microsoft teams etc.",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public workspaceType?: WorkspaceType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Misc Data",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.JSON,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public miscData?: MiscData = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "user",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "Relation to User who this email belongs to",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "User ID",
|
||||
description: "User ID who this email belongs to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
@Index()
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default WorkspaceUserAuthToken;
|
||||
@@ -131,6 +131,30 @@ export default class BaseAPI<
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/:id/update-item`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.updateItem(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/:id/update-item`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.updateItem(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete
|
||||
router.delete(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/:id`,
|
||||
@@ -144,6 +168,30 @@ export default class BaseAPI<
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/:id/delete-item`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.deleteItem(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/:id/delete-item`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.deleteItem(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router = router;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
368
Common/Server/API/SlackAPI.ts
Normal file
368
Common/Server/API/SlackAPI.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import SlackAuthorization from "../Middleware/SlackAuthorization";
|
||||
import BadRequestException from "../../Types/Exception/BadRequestException";
|
||||
import logger from "../Utils/Logger";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import {
|
||||
AppApiClientUrl,
|
||||
DashboardClientUrl,
|
||||
SlackAppClientId,
|
||||
SlackAppClientSecret,
|
||||
} from "../EnvironmentConfig";
|
||||
import SlackAppManifest from "../Utils/Workspace/Slack/app-manifest.json";
|
||||
import URL from "../../Types/API/URL";
|
||||
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../Types/API/HTTPResponse";
|
||||
import API from "../../Utils/API";
|
||||
import WorkspaceProjectAuthTokenService from "../Services/WorkspaceProjectAuthTokenService";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceUserAuthTokenService from "../Services/WorkspaceUserAuthTokenService";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
|
||||
export default class SlackAPI {
|
||||
public getRouter(): ExpressRouter {
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.get(
|
||||
"/slack/app-manifest",
|
||||
(req: ExpressRequest, res: ExpressResponse) => {
|
||||
// return app manifest for slack app
|
||||
return Response.sendJsonObjectResponse(req, res, SlackAppManifest);
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/slack/auth/:projectId/:userId",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
if (!SlackAppClientId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Slack App Client ID is not set"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!SlackAppClientSecret) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Slack App Client Secret is not set"),
|
||||
);
|
||||
}
|
||||
|
||||
const projectId: string | undefined =
|
||||
req.params["projectId"]?.toString();
|
||||
const userId: string | undefined = req.params["userId"]?.toString();
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid ProjectID in request"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid UserID in request"),
|
||||
);
|
||||
}
|
||||
|
||||
// if there's an error query param.
|
||||
const error: string | undefined = req.query["error"]?.toString();
|
||||
|
||||
const slackIntegrationPageUrl: URL = URL.fromString(
|
||||
DashboardClientUrl.toString() +
|
||||
`/${projectId.toString()}/settings/slack-integration`,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
slackIntegrationPageUrl.addQueryParam("error", error),
|
||||
);
|
||||
}
|
||||
|
||||
// slack returns the code on successful auth.
|
||||
const code: string | undefined = req.query["code"]?.toString();
|
||||
|
||||
if (!code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException("Invalid request"),
|
||||
);
|
||||
}
|
||||
|
||||
// get access token from slack api.
|
||||
|
||||
const redirectUri: URL = URL.fromString(
|
||||
`${AppApiClientUrl.toString()}/slack/auth/${projectId}/${userId}`,
|
||||
);
|
||||
|
||||
const requestBody: JSONObject = {
|
||||
code: code,
|
||||
client_id: SlackAppClientId,
|
||||
client_secret: SlackAppClientSecret,
|
||||
redirect_uri: redirectUri.toString(),
|
||||
};
|
||||
|
||||
logger.debug("Slack Auth Request Body: ");
|
||||
logger.debug(requestBody);
|
||||
|
||||
// send the request to slack api to get the access token https://slack.com/api/oauth.v2.access
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/oauth.v2.access"),
|
||||
requestBody,
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const responseBody: JSONObject = response.data;
|
||||
|
||||
logger.debug("Slack Auth Request Body: ");
|
||||
logger.debug(responseBody);
|
||||
|
||||
let slackTeamId: string | undefined = undefined;
|
||||
let slackBotAccessToken: string | undefined = undefined;
|
||||
let slackUserId: string | undefined = undefined;
|
||||
let slackTeamName: string | undefined = undefined;
|
||||
let botUserId: string | undefined = undefined;
|
||||
let slackUserAccessToken: string | undefined = undefined;
|
||||
|
||||
// ReponseBody is in this format.
|
||||
// {
|
||||
// "ok": true,
|
||||
// "access_token": "sample-token",
|
||||
// "token_type": "bot",
|
||||
// "scope": "commands,incoming-webhook",
|
||||
// "bot_user_id": "U0KRQLJ9H",
|
||||
// "app_id": "A0KRD7HC3",
|
||||
// "team": {
|
||||
// "name": "Slack Pickleball Team",
|
||||
// "id": "T9TK3CUKW"
|
||||
// },
|
||||
// "enterprise": {
|
||||
// "name": "slack-pickleball",
|
||||
// "id": "E12345678"
|
||||
// },
|
||||
// "authed_user": {
|
||||
// "id": "U1234",
|
||||
// "scope": "chat:write",
|
||||
// "access_token": "sample-token",
|
||||
// "token_type": "user"
|
||||
// }
|
||||
// }
|
||||
|
||||
if (responseBody["ok"] !== true) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException("Invalid request"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
responseBody["team"] &&
|
||||
(responseBody["team"] as JSONObject)["id"]
|
||||
) {
|
||||
slackTeamId = (responseBody["team"] as JSONObject)["id"]?.toString();
|
||||
}
|
||||
|
||||
if (responseBody["access_token"]) {
|
||||
slackBotAccessToken = responseBody["access_token"]?.toString();
|
||||
}
|
||||
|
||||
if (
|
||||
responseBody["authed_user"] &&
|
||||
(responseBody["authed_user"] as JSONObject)["id"]
|
||||
) {
|
||||
slackUserId = (responseBody["authed_user"] as JSONObject)[
|
||||
"id"
|
||||
]?.toString();
|
||||
}
|
||||
|
||||
if (
|
||||
responseBody["authed_user"] &&
|
||||
(responseBody["authed_user"] as JSONObject)["access_token"]
|
||||
) {
|
||||
slackUserAccessToken = (responseBody["authed_user"] as JSONObject)[
|
||||
"access_token"
|
||||
]?.toString();
|
||||
}
|
||||
|
||||
if (
|
||||
responseBody["team"] &&
|
||||
(responseBody["team"] as JSONObject)["name"]
|
||||
) {
|
||||
slackTeamName = (responseBody["team"] as JSONObject)[
|
||||
"name"
|
||||
]?.toString();
|
||||
}
|
||||
|
||||
if (responseBody["bot_user_id"]) {
|
||||
botUserId = responseBody["bot_user_id"]?.toString();
|
||||
}
|
||||
|
||||
await WorkspaceProjectAuthTokenService.refreshAuthToken({
|
||||
projectId: new ObjectID(projectId),
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
authToken: slackBotAccessToken || "",
|
||||
workspaceProjectId: slackTeamId || "",
|
||||
miscData: {
|
||||
teamId: slackTeamId || "",
|
||||
teamName: slackTeamName || "",
|
||||
botUserId: botUserId || "",
|
||||
},
|
||||
});
|
||||
|
||||
await WorkspaceUserAuthTokenService.refreshAuthToken({
|
||||
projectId: new ObjectID(projectId),
|
||||
userId: new ObjectID(userId),
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
authToken: slackUserAccessToken || "",
|
||||
workspaceUserId: slackUserId || "",
|
||||
miscData: {
|
||||
userId: slackUserId || "",
|
||||
},
|
||||
});
|
||||
|
||||
// return back to dashboard after successful auth.
|
||||
Response.redirect(req, res, slackIntegrationPageUrl);
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/slack/interactive",
|
||||
SlackAuthorization.isAuthorizedSlackRequest,
|
||||
(req: ExpressRequest, res: ExpressResponse) => {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// options load endpoint.
|
||||
|
||||
router.post(
|
||||
"/slack/options-load",
|
||||
SlackAuthorization.isAuthorizedSlackRequest,
|
||||
(req: ExpressRequest, res: ExpressResponse) => {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/slack/command",
|
||||
SlackAuthorization.isAuthorizedSlackRequest,
|
||||
(req: ExpressRequest, res: ExpressResponse) => {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/slack/events",
|
||||
SlackAuthorization.isAuthorizedSlackRequest,
|
||||
(req: ExpressRequest, res: ExpressResponse) => {
|
||||
// respond to slack challenge
|
||||
|
||||
const body: any = req.body;
|
||||
|
||||
if (body.challenge) {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
challenge: body.challenge,
|
||||
});
|
||||
}
|
||||
|
||||
// if event is "create-incident" then show the incident create modal with title and description and add a button to submit the form.
|
||||
|
||||
if (body.event && body.event.type === "create-incident") {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
type: "modal",
|
||||
title: {
|
||||
type: "plain_text",
|
||||
text: "Create Incident",
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "input",
|
||||
block_id: "title",
|
||||
element: {
|
||||
type: "plain_text_input",
|
||||
action_id: "title",
|
||||
placeholder: {
|
||||
type: "plain_text",
|
||||
text: "Incident Title",
|
||||
},
|
||||
},
|
||||
label: {
|
||||
type: "plain_text",
|
||||
text: "Title",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
block_id: "description",
|
||||
element: {
|
||||
type: "plain_text_input",
|
||||
action_id: "description",
|
||||
placeholder: {
|
||||
type: "plain_text",
|
||||
text: "Incident Description",
|
||||
},
|
||||
},
|
||||
label: {
|
||||
type: "plain_text",
|
||||
text: "Description",
|
||||
},
|
||||
},
|
||||
// button
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Submit",
|
||||
},
|
||||
style: "primary",
|
||||
value: "submit",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException("Invalid request"),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,8 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"
|
||||
import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
|
||||
import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
|
||||
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
||||
import StatusPageResourceUptimeUtil from "../../Utils/StatusPage/ResourceUptime";
|
||||
import MonitorService from "../Services/MonitorService";
|
||||
|
||||
export default class StatusPageAPI extends BaseAPI<
|
||||
StatusPage,
|
||||
@@ -517,6 +519,128 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/overview/:statusPageId/uptime-percent`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
// This reosurce ID can be of a status page resource OR a status page group.
|
||||
const statusPageResourceId: ObjectID = new ObjectID(
|
||||
req.params["statusPageResourceId"] as string,
|
||||
);
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(
|
||||
req.params["statusPageId"] as string,
|
||||
);
|
||||
|
||||
if(!statusPageId || !statusPageResourceId){
|
||||
throw new BadDataException("Status Page or Resource not found");
|
||||
}
|
||||
|
||||
// get start and end date from request body.
|
||||
// if no end date is provided then it will be current date.
|
||||
// if no start date is provided then it will be 14 days ago from end date.
|
||||
|
||||
let startDate: Date = OneUptimeDate.getSomeDaysAgo(14)
|
||||
let endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if(req.body["startDate"]){
|
||||
startDate = OneUptimeDate.fromString(req.body["startDate"] as string);
|
||||
}
|
||||
|
||||
if(req.body["endDate"]){
|
||||
endDate = OneUptimeDate.fromString(req.body["endDate"] as string);
|
||||
}
|
||||
|
||||
const monitorStatusTimelines: Array<MonitorStatusTimeline> = [];
|
||||
|
||||
|
||||
// get monitor or group.
|
||||
|
||||
// get status page group.
|
||||
|
||||
const monitorsInResource: Array<ObjectID> = [];
|
||||
|
||||
const statusPageGroup: StatusPageGroup | null = await StatusPageGroupService.findOneBy({
|
||||
query: {
|
||||
_id: statusPageResourceId,
|
||||
statusPageId: statusPageId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
statusPageId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if(statusPageGroup){
|
||||
// get all monitors in group.
|
||||
const groupResources: Array<StatusPageResource> = await StatusPageResourceService.findBy({
|
||||
query: {
|
||||
statusPageGroupId: statusPageResourceId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
monitorGroupId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
monitorsInGroup.push(...groupResources.map((resource: StatusPageResource) => {
|
||||
return resource.monitorId!;
|
||||
}).filter((id: ObjectID) => {
|
||||
return Boolean(id);
|
||||
}));
|
||||
|
||||
|
||||
}
|
||||
|
||||
const monitor: Monitor | null = await MonitorService.findOneBy({
|
||||
query: {
|
||||
_id: statusPageResourceId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
monitorGroupId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const uptimePercent: number | null = null;
|
||||
StatusPageResourceUptimeUtil.calculateAvgUptimePercentOfStatusPageGroup(
|
||||
{
|
||||
statusPageGroup: data.group,
|
||||
monitorStatusTimelines: monitorStatusTimelines,
|
||||
precision:
|
||||
data.group.uptimePercentPrecision ||
|
||||
UptimePrecision.ONE_DECIMAL,
|
||||
downtimeMonitorStatuses:
|
||||
statusPage?.downtimeMonitorStatuses || [],
|
||||
statusPageResources: statusPageResources,
|
||||
monitorsInGroup: monitorsInGroup,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
next(err);
|
||||
|
||||
}
|
||||
});
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
|
||||
@@ -30,6 +30,14 @@ export default class DatabaseConfig {
|
||||
return globalConfig.getColumnValue(key);
|
||||
}
|
||||
|
||||
public static async getHomeUrl(): Promise<URL> {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
return new URL(httpProtocol, host);
|
||||
}
|
||||
|
||||
public static async getHost(): Promise<Hostname> {
|
||||
return Promise.resolve(new Hostname(process.env["HOST"] || "localhost"));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AccountsRoute,
|
||||
AdminDashboardRoute,
|
||||
DashboardRoute,
|
||||
AppApiRoute,
|
||||
} from "Common/ServiceRoute";
|
||||
import BillingConfig from "./BillingConfig";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
@@ -259,6 +260,10 @@ export const WorkflowScriptTimeoutInMS: number = process.env[
|
||||
? parseInt(process.env["WORKFLOW_SCRIPT_TIMEOUT_IN_MS"].toString())
|
||||
: 5000;
|
||||
|
||||
export const WorkflowTimeoutInMs: number = process.env["WORKFLOW_TIMEOUT_IN_MS"]
|
||||
? parseInt(process.env["WORKFLOW_TIMEOUT_IN_MS"].toString())
|
||||
: 5000;
|
||||
|
||||
export const AllowedActiveMonitorCountInFreePlan: number = process.env[
|
||||
"ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN"
|
||||
]
|
||||
@@ -279,8 +284,19 @@ export const AllowedSubscribersCountInFreePlan: number = process.env[
|
||||
? parseInt(process.env["ALLOWED_SUBSCRIBERS_COUNT_IN_FREE_PLAN"].toString())
|
||||
: 100;
|
||||
|
||||
export const NotificationWebhookOnCreateUser: string =
|
||||
process.env["NOTIFICATION_WEBHOOK_ON_CREATED_USER"] || "";
|
||||
export const NotificationSlackWebhookOnCreateUser: string =
|
||||
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER"] || "";
|
||||
|
||||
export const NotificationSlackWebhookOnCreateProject: string =
|
||||
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT"] || "";
|
||||
|
||||
// notification delete project
|
||||
export const NotificationSlackWebhookOnDeleteProject: string =
|
||||
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT"] || "";
|
||||
|
||||
// notification subscripton update.
|
||||
export const NotificationSlackWebhookOnSubscriptionUpdate: string =
|
||||
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE"] || "";
|
||||
|
||||
export const AdminDashboardClientURL: URL = new URL(
|
||||
HttpProtocol,
|
||||
@@ -288,6 +304,8 @@ export const AdminDashboardClientURL: URL = new URL(
|
||||
AdminDashboardRoute,
|
||||
);
|
||||
|
||||
export const AppApiClientUrl: URL = new URL(HttpProtocol, Host, AppApiRoute);
|
||||
|
||||
export const DashboardClientUrl: URL = new URL(
|
||||
HttpProtocol,
|
||||
Host,
|
||||
@@ -302,3 +320,10 @@ export const AccountsClientUrl: URL = new URL(
|
||||
|
||||
export const DisableTelemetry: boolean =
|
||||
process.env["DISABLE_TELEMETRY"] === "true";
|
||||
|
||||
export const SlackAppClientId: string | null =
|
||||
process.env["SLACK_APP_CLIENT_ID"] || null;
|
||||
export const SlackAppClientSecret: string | null =
|
||||
process.env["SLACK_APP_CLIENT_SECRET"] || null;
|
||||
export const SlackAppSigningSecret: string | null =
|
||||
process.env["SLACK_APP_SIGNING_SECRET"] || null;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1737713529424 implements MigrationInterface {
|
||||
public name = "MigrationName1737713529424";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" character varying(100)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1737715240684 implements MigrationInterface {
|
||||
public name = "MigrationName1737715240684";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" character varying(100)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1737997557974 implements MigrationInterface {
|
||||
public name = "MigrationName1737997557974";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4d5e62631b2b63aaecb00950ef" ON "MonitorTest" ("isInQueue") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_4d5e62631b2b63aaecb00950ef"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739209832500 implements MigrationInterface {
|
||||
public name = "MigrationName1739209832500";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WorkspaceUserAuthToken" ("_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, "authToken" text NOT NULL, "workspaceUserId" character varying(500) NOT NULL, "workspaceType" character varying(500) NOT NULL, "miscData" jsonb NOT NULL, "userId" uuid, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_ae2f1b46b7e26f58a1f4a56b6ea" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_bee888f5782b9585e01f13455f" ON "WorkspaceUserAuthToken" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4b7c7d1a8b2259df8c790db094" ON "WorkspaceUserAuthToken" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WorkspaceProjectAuthToken" ("_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, "authToken" text NOT NULL, "workspaceType" character varying(500) NOT NULL, "workspaceProjectId" character varying(500) NOT NULL, "miscData" jsonb NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_c0caa6a69da614ee74d8c1291da" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_73f5887268b09c0abccf04ef02" ON "WorkspaceProjectAuthToken" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WorkspaceSetting" ("_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, "settings" jsonb NOT NULL, "workspaceType" character varying(500) NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_eb98d42edd6489fbe1cf3f34515" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c68f38e2b2b061c40209e85bf2" ON "WorkspaceSetting" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WorkspaceNotificationRule" ("_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, "name" character varying(500) NOT NULL, "description" character varying(500), "notificationRule" jsonb NOT NULL, "eventType" character varying NOT NULL, "workspaceType" character varying(500) NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_d1485681c7695ac9841dc52a451" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_349b022afa9a50a597d6c91ec9" ON "WorkspaceNotificationRule" ("projectId") `,
|
||||
);
|
||||
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 "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_bee888f5782b9585e01f13455fb" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_4b7c7d1a8b2259df8c790db0940" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_ec5cbf4536681fe4bea883c98ea" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_1b2cb71eaf9e665e4556d1b1263" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_73f5887268b09c0abccf04ef02e" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_8aa5804c7a728039564bf5d967d" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_6287095997a16f1cbdd4fb24b61" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_c68f38e2b2b061c40209e85bf22" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_c8fdd61b95bfd0a2ca268b8c602" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_cb3b7931417a4b4ee05d487b614" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_349b022afa9a50a597d6c91ec95" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_55f4e43427fc217ed32cf640a28" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_65ac673d16286be2dcd5229fe24" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_65ac673d16286be2dcd5229fe24"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_55f4e43427fc217ed32cf640a28"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_349b022afa9a50a597d6c91ec95"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_cb3b7931417a4b4ee05d487b614"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_c8fdd61b95bfd0a2ca268b8c602"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_c68f38e2b2b061c40209e85bf22"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_6287095997a16f1cbdd4fb24b61"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_8aa5804c7a728039564bf5d967d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_73f5887268b09c0abccf04ef02e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_1b2cb71eaf9e665e4556d1b1263"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_ec5cbf4536681fe4bea883c98ea"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_4b7c7d1a8b2259df8c790db0940"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_bee888f5782b9585e01f13455fb"`,
|
||||
);
|
||||
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_349b022afa9a50a597d6c91ec9"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WorkspaceNotificationRule"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_c68f38e2b2b061c40209e85bf2"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WorkspaceSetting"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_73f5887268b09c0abccf04ef02"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WorkspaceProjectAuthToken"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_4b7c7d1a8b2259df8c790db094"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_bee888f5782b9585e01f13455f"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WorkspaceUserAuthToken"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739210586538 implements MigrationInterface {
|
||||
public name = "MigrationName1739210586538";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Alert" ADD "alertNumber" integer`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_aa91b2228a2b35424a3ae93fdc" ON "Alert" ("alertNumber") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_aa91b2228a2b35424a3ae93fdc"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "Alert" DROP COLUMN "alertNumber"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739217257089 implements MigrationInterface {
|
||||
public name = "MigrationName1739217257089";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" ADD "scheduledMaintenanceNumber" integer`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_207fe82fd8bdc67bbe1aa0ebf8" ON "ScheduledMaintenance" ("scheduledMaintenanceNumber") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_207fe82fd8bdc67bbe1aa0ebf8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "scheduledMaintenanceNumber"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739282331053 implements MigrationInterface {
|
||||
public name = "MigrationName1739282331053";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "postUpdatesToSlackChannelId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" ADD "postUpdatesToSlackChannelId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToSlackChannelId" character varying(100)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739374537088 implements MigrationInterface {
|
||||
public name = "MigrationName1739374537088";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1739569321582 implements MigrationInterface {
|
||||
public name = "MigrationName1739569321582";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "postUpdatesToWorkspaceChannels" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" ADD "postUpdatesToWorkspaceChannels" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToWorkspaceChannels" jsonb`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,15 @@ import { MigrationName1736787985322 } from "./1736787985322-MigrationName";
|
||||
import { MigrationName1736788706141 } from "./1736788706141-MigrationName";
|
||||
import { MigrationName1736856662868 } from "./1736856662868-MigrationName";
|
||||
import { MigrationName1737141420441 } from "./1737141420441-MigrationName";
|
||||
import { MigrationName1737713529424 } from "./1737713529424-MigrationName";
|
||||
import { MigrationName1737715240684 } from "./1737715240684-MigrationName";
|
||||
import { MigrationName1737997557974 } from "./1737997557974-MigrationName";
|
||||
import { MigrationName1739209832500 } from "./1739209832500-MigrationName";
|
||||
import { MigrationName1739210586538 } from "./1739210586538-MigrationName";
|
||||
import { MigrationName1739217257089 } from "./1739217257089-MigrationName";
|
||||
import { MigrationName1739282331053 } from "./1739282331053-MigrationName";
|
||||
import { MigrationName1739374537088 } from "./1739374537088-MigrationName";
|
||||
import { MigrationName1739569321582 } from "./1739569321582-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -196,4 +205,13 @@ export default [
|
||||
MigrationName1736788706141,
|
||||
MigrationName1736856662868,
|
||||
MigrationName1737141420441,
|
||||
MigrationName1737713529424,
|
||||
MigrationName1737715240684,
|
||||
MigrationName1737997557974,
|
||||
MigrationName1739209832500,
|
||||
MigrationName1739210586538,
|
||||
MigrationName1739217257089,
|
||||
MigrationName1739282331053,
|
||||
MigrationName1739374537088,
|
||||
MigrationName1739569321582,
|
||||
];
|
||||
|
||||
60
Common/Server/Middleware/SlackAuthorization.ts
Normal file
60
Common/Server/Middleware/SlackAuthorization.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { SlackAppSigningSecret } from "../EnvironmentConfig";
|
||||
import crypto from "crypto";
|
||||
import logger from "../Utils/Logger";
|
||||
|
||||
export default class SlackAuthorization {
|
||||
public static async isAuthorizedSlackRequest(
|
||||
req: OneUptimeRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> {
|
||||
if (!SlackAppSigningSecret) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"SLACK_APP_SIGNING_SECRET env variable not found.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// validate slack signing secret
|
||||
const slackSigningSecret: string = SlackAppSigningSecret.toString();
|
||||
|
||||
const slackSignature: string = req.headers["x-slack-signature"] as string;
|
||||
const timestamp: string = req.headers[
|
||||
"x-slack-request-timestamp"
|
||||
] as string;
|
||||
const requestBody: string = req.body;
|
||||
|
||||
logger.debug(`slackSignature: ${slackSignature}`);
|
||||
logger.debug(`timestamp: ${timestamp}`);
|
||||
logger.debug(`requestBody: ${requestBody}`);
|
||||
|
||||
const baseString: string = `v0:${timestamp}:${requestBody}`;
|
||||
const signature: string = `v0=${crypto.createHmac("sha256", slackSigningSecret).update(baseString).digest("hex")}`;
|
||||
|
||||
// check if the signature is valid
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(slackSignature),
|
||||
)
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Slack Signature Verification Failed."),
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from "../../Server/Utils/Express";
|
||||
import TelemetryIngestionKeyService from "../../Server/Services/TelemetryIngestionKeyService";
|
||||
import TelemetryIngestionKey from "../../Models/DatabaseModels/TelemetryIngestionKey";
|
||||
import Response from "../Utils/Response";
|
||||
import logger from "../Utils/Logger";
|
||||
|
||||
export interface TelemetryRequest extends ExpressRequest {
|
||||
projectId: ObjectID; // Project ID
|
||||
@@ -17,12 +19,14 @@ export interface TelemetryRequest extends ExpressRequest {
|
||||
export default class TelemetryIngest {
|
||||
public static async isAuthorizedServiceMiddleware(
|
||||
req: ExpressRequest,
|
||||
_res: ExpressResponse,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// check header.
|
||||
|
||||
const isOpenTelemetryAPI: boolean = req.path.includes("/otlp/v1");
|
||||
|
||||
let oneuptimeToken: string | undefined = req.headers[
|
||||
"x-oneuptime-token"
|
||||
] as string | undefined;
|
||||
@@ -35,6 +39,14 @@ export default class TelemetryIngest {
|
||||
}
|
||||
|
||||
if (!oneuptimeToken) {
|
||||
logger.error("Missing header: x-oneuptime-token");
|
||||
|
||||
if (isOpenTelemetryAPI) {
|
||||
// then accept the response and return success.
|
||||
// do not return error because it causes Otel to retry the request.
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
}
|
||||
|
||||
throw new BadRequestException("Missing header: x-oneuptime-token");
|
||||
}
|
||||
|
||||
@@ -54,6 +66,14 @@ export default class TelemetryIngest {
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
logger.error("Invalid service token: " + oneuptimeToken);
|
||||
|
||||
if (isOpenTelemetryAPI) {
|
||||
// then accept the response and return success.
|
||||
// do not return error because it causes Otel to retry the request.
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
"Invalid service token: " + oneuptimeToken,
|
||||
);
|
||||
@@ -62,6 +82,16 @@ export default class TelemetryIngest {
|
||||
projectId = token.projectId as ObjectID;
|
||||
|
||||
if (!projectId) {
|
||||
logger.error(
|
||||
"Project ID not found for service token: " + oneuptimeToken,
|
||||
);
|
||||
|
||||
if (isOpenTelemetryAPI) {
|
||||
// then accept the response and return success.
|
||||
// do not return error because it causes Otel to retry the request.
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
"Project ID not found for service token: " + oneuptimeToken,
|
||||
);
|
||||
|
||||
@@ -41,6 +41,10 @@ import AlertMetricType from "../../Types/Alerts/AlertMetricType";
|
||||
import AlertFeedService from "./AlertFeedService";
|
||||
import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
|
||||
import { Gray500, Red500 } from "../../Types/BrandColors";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
import LabelService from "./LabelService";
|
||||
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
||||
import AlertSeverityService from "./AlertSeverityService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -94,6 +98,32 @@ export class Service extends DatabaseService<Model> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getExistingAlertNumberForProject(data: {
|
||||
projectId: ObjectID;
|
||||
}): Promise<number> {
|
||||
// get last alert number.
|
||||
const lastAlert: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
},
|
||||
select: {
|
||||
alertNumber: true,
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!lastAlert) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return lastAlert.alertNumber || 0;
|
||||
}
|
||||
|
||||
public async acknowledgeAlert(
|
||||
alertId: ObjectID,
|
||||
acknowledgedByUserId: ObjectID,
|
||||
@@ -152,9 +182,12 @@ export class Service extends DatabaseService<Model> {
|
||||
throw new BadDataException("ProjectId required to create alert.");
|
||||
}
|
||||
|
||||
const projectId: ObjectID =
|
||||
createBy.props.tenantId || createBy.data.projectId!;
|
||||
|
||||
const alertState: AlertState | null = await AlertStateService.findOneBy({
|
||||
query: {
|
||||
projectId: createBy.props.tenantId || createBy.data.projectId!,
|
||||
projectId: projectId,
|
||||
isCreatedState: true,
|
||||
},
|
||||
select: {
|
||||
@@ -173,6 +206,13 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
createBy.data.currentAlertStateId = alertState.id;
|
||||
|
||||
const alertNumberForThisAlert: number =
|
||||
(await this.getExistingAlertNumberForProject({
|
||||
projectId: projectId,
|
||||
})) + 1;
|
||||
|
||||
createBy.data.alertNumber = alertNumberForThisAlert;
|
||||
|
||||
if (
|
||||
(createBy.data.createdByUserId ||
|
||||
createBy.data.createdByUser ||
|
||||
@@ -235,7 +275,7 @@ export class Service extends DatabaseService<Model> {
|
||||
projectId: createdItem.projectId!,
|
||||
alertFeedEventType: AlertFeedEventType.AlertCreated,
|
||||
displayColor: Red500,
|
||||
feedInfoInMarkdown: `**Alert Created**:
|
||||
feedInfoInMarkdown: `**Alert #${createdItem.alertNumber?.toString()} Created**:
|
||||
|
||||
**Alert Title**:
|
||||
|
||||
@@ -492,74 +532,132 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
|
||||
|
||||
if (updatedItemIds.length > 0) {
|
||||
for (const alertId of updatedItemIds) {
|
||||
let shouldAddAlertFeed: boolean = false;
|
||||
let feedInfoInMarkdown: string = "**Alert was updated.**";
|
||||
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
if (onUpdate.updateBy.data.title) {
|
||||
// add alert feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
await AlertFeedService.createAlertFeed({
|
||||
alertId: alertId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
alertFeedEventType: AlertFeedEventType.AlertUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Alert title was updated.** Here's the new title.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Title**:
|
||||
${onUpdate.updateBy.data.title || "No title provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
`;
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.rootCause) {
|
||||
// add alert feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
if (onUpdate.updateBy.data.title) {
|
||||
// add alert feed.
|
||||
|
||||
await AlertFeedService.createAlertFeed({
|
||||
alertId: alertId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
alertFeedEventType: AlertFeedEventType.AlertUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Alert root cause was updated.** Here's the new root cause.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Root Cause**:
|
||||
${onUpdate.updateBy.data.rootCause || "No root cause provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
`;
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.description) {
|
||||
// add alert feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
await AlertFeedService.createAlertFeed({
|
||||
alertId: alertId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
alertFeedEventType: AlertFeedEventType.AlertUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Alert description was updated.** Here's the new description.
|
||||
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
feedInfoInMarkdown += `\n\n**Alert Description**:
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`;
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.remediationNotes) {
|
||||
// add alert feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Remediation Notes**:
|
||||
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
|
||||
`;
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.labels &&
|
||||
onUpdate.updateBy.data.labels.length > 0 &&
|
||||
Array.isArray(onUpdate.updateBy.data.labels)
|
||||
) {
|
||||
const labelIds: Array<ObjectID> = (
|
||||
onUpdate.updateBy.data.labels as any
|
||||
)
|
||||
.map((label: Label) => {
|
||||
if (label._id) {
|
||||
return new ObjectID(label._id?.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((labelId: ObjectID | null) => {
|
||||
return labelId !== null;
|
||||
});
|
||||
|
||||
const labels: Array<Label> = await LabelService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(labelIds),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (labels.length > 0) {
|
||||
feedInfoInMarkdown += `\n\n**Labels**:
|
||||
|
||||
${labels
|
||||
.map((label: Label) => {
|
||||
return `- ${label.name}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.alertSeverity &&
|
||||
(onUpdate.updateBy.data.alertSeverity as any)._id
|
||||
) {
|
||||
const alertSeverity: AlertSeverity | null =
|
||||
await AlertSeverityService.findOneBy({
|
||||
query: {
|
||||
_id: new ObjectID(
|
||||
(onUpdate.updateBy.data.alertSeverity as any)?._id.toString(),
|
||||
),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alertSeverity) {
|
||||
feedInfoInMarkdown += `\n\n**Alert Severity**:
|
||||
${alertSeverity.name}
|
||||
`;
|
||||
|
||||
shouldAddAlertFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAddAlertFeed) {
|
||||
await AlertFeedService.createAlertFeed({
|
||||
alertId: alertId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
alertFeedEventType: AlertFeedEventType.AlertUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Remediation notes were updated.** Here are the new notes.
|
||||
|
||||
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
|
||||
`,
|
||||
feedInfoInMarkdown: feedInfoInMarkdown,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import logger from "../Utils/Logger";
|
||||
import Realtime from "../Utils/Realtime";
|
||||
import StreamUtil from "../Utils/Stream";
|
||||
import BaseService from "./BaseService";
|
||||
import { ExecResult } from "@clickhouse/client";
|
||||
import { ExecResult, ResponseJSON, ResultSet } from "@clickhouse/client";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import { WorkflowRoute } from "Common/ServiceRoute";
|
||||
import Protocol from "../../Types/API/Protocol";
|
||||
@@ -148,19 +148,29 @@ export default class AnalyticsDatabaseService<
|
||||
|
||||
const countStatement: Statement = this.toCountStatement(countBy);
|
||||
|
||||
const dbResult: ExecResult<Stream> = await this.execute(countStatement);
|
||||
const dbResult: ResultSet<"JSON"> =
|
||||
await this.executeQuery(countStatement);
|
||||
|
||||
const strResult: string = await StreamUtil.convertStreamToText(
|
||||
dbResult.stream,
|
||||
);
|
||||
logger.debug(`${this.model.tableName} Count Statement executed`);
|
||||
logger.debug(countStatement);
|
||||
|
||||
let countPositive: PositiveNumber = new PositiveNumber(strResult || 0);
|
||||
|
||||
if (countBy.groupBy && Object.keys(countBy.groupBy).length > 0) {
|
||||
// this usually happens when group by is used. In this case we count the total number of groups and not rows in those groups.
|
||||
countPositive = new PositiveNumber(strResult.split("\n").length - 1); // -1 because the last line is empty.
|
||||
const resultInJSON: ResponseJSON<JSONObject> =
|
||||
await dbResult.json<JSONObject>();
|
||||
let countPositive: PositiveNumber = new PositiveNumber(0);
|
||||
if (
|
||||
resultInJSON.data &&
|
||||
resultInJSON.data[0] &&
|
||||
resultInJSON.data[0]["count()"] &&
|
||||
typeof resultInJSON.data[0]["count()"] === "string"
|
||||
) {
|
||||
countPositive = new PositiveNumber(
|
||||
resultInJSON.data[0]["count()"] as string,
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Result: `);
|
||||
logger.debug(countPositive.toNumber());
|
||||
|
||||
countPositive = await this.onCountSuccess(countPositive);
|
||||
return countPositive;
|
||||
} catch (error) {
|
||||
@@ -240,20 +250,18 @@ export default class AnalyticsDatabaseService<
|
||||
columns: Array<string>;
|
||||
} = this.toAggregateStatement(aggregateBy);
|
||||
|
||||
const dbResult: ExecResult<Stream> = await this.execute(
|
||||
const dbResult: ResultSet<"JSON"> = await this.executeQuery(
|
||||
findStatement.statement,
|
||||
);
|
||||
|
||||
const strResult: string = await StreamUtil.convertStreamToText(
|
||||
dbResult.stream,
|
||||
);
|
||||
logger.debug(`${this.model.tableName} Aggregate Statement executed`);
|
||||
|
||||
const jsonItems: Array<JSONObject> = this.convertSelectReturnedDataToJson(
|
||||
strResult,
|
||||
findStatement.columns,
|
||||
);
|
||||
const responseJSON: ResponseJSON<JSONObject> =
|
||||
await dbResult.json<JSONObject>();
|
||||
|
||||
const items: Array<JSONObject> = jsonItems as any;
|
||||
const items: Array<JSONObject> = responseJSON.data
|
||||
? responseJSON.data
|
||||
: [];
|
||||
|
||||
const aggregatedItems: Array<AggregatedModel> = [];
|
||||
|
||||
@@ -314,16 +322,6 @@ export default class AnalyticsDatabaseService<
|
||||
}
|
||||
}
|
||||
|
||||
public async executeQuery(query: string): Promise<string> {
|
||||
const dbResult: ExecResult<Stream> = await this.execute(query);
|
||||
|
||||
const strResult: string = await StreamUtil.convertStreamToText(
|
||||
dbResult.stream,
|
||||
);
|
||||
|
||||
return strResult;
|
||||
}
|
||||
|
||||
private async _findBy(
|
||||
findBy: FindBy<TBaseModel>,
|
||||
): Promise<Array<TBaseModel>> {
|
||||
@@ -379,21 +377,17 @@ export default class AnalyticsDatabaseService<
|
||||
columns: Array<string>;
|
||||
} = this.toFindStatement(onBeforeFind);
|
||||
|
||||
const dbResult: ExecResult<Stream> = await this.execute(
|
||||
const dbResult: ResultSet<"JSON"> = await this.executeQuery(
|
||||
findStatement.statement,
|
||||
);
|
||||
|
||||
logger.debug(`${this.model.tableName} Find Statement executed`);
|
||||
logger.debug(findStatement.statement);
|
||||
|
||||
const strResult: string = await StreamUtil.convertStreamToText(
|
||||
dbResult.stream,
|
||||
);
|
||||
const responseJSON: ResponseJSON<JSONObject> =
|
||||
await dbResult.json<JSONObject>();
|
||||
|
||||
const jsonItems: Array<JSONObject> = this.convertSelectReturnedDataToJson(
|
||||
strResult,
|
||||
findStatement.columns,
|
||||
);
|
||||
const jsonItems: Array<JSONObject> = responseJSON.data;
|
||||
|
||||
let items: Array<TBaseModel> =
|
||||
AnalyticsBaseModel.fromJSONArray<TBaseModel>(jsonItems, this.modelType);
|
||||
@@ -411,7 +405,7 @@ export default class AnalyticsDatabaseService<
|
||||
}
|
||||
}
|
||||
|
||||
private convertSelectReturnedDataToJson(
|
||||
public convertSelectReturnedDataToJson(
|
||||
strResult: string,
|
||||
columns: string[],
|
||||
): JSONObject[] {
|
||||
@@ -815,6 +809,24 @@ export default class AnalyticsDatabaseService<
|
||||
) as ExecResult<Stream>;
|
||||
}
|
||||
|
||||
|
||||
public async executeQuery(
|
||||
statement: Statement | string,
|
||||
): Promise<ResultSet<"JSON">> {
|
||||
if (!this.databaseClient) {
|
||||
this.useDefaultDatabase();
|
||||
}
|
||||
|
||||
const query: string = statement instanceof Statement ? statement.query : statement;
|
||||
const queryParams: Record<string, unknown> | undefined = statement instanceof Statement ? statement.query_params : undefined;
|
||||
|
||||
return await this.databaseClient.query({
|
||||
query: query,
|
||||
format: "JSON",
|
||||
query_params: queryParams || undefined as any, // undefined is not specified in the type for query_params, but its ok to pass undefined.
|
||||
})
|
||||
}
|
||||
|
||||
protected async onUpdateSuccess(
|
||||
onUpdate: OnUpdate<TBaseModel>,
|
||||
_updatedItemIds: Array<ObjectID>,
|
||||
|
||||
@@ -50,6 +50,17 @@ import Semaphore, {
|
||||
import IncidentFeedService from "./IncidentFeedService";
|
||||
import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed";
|
||||
import { Gray500, Red500 } from "../../Types/BrandColors";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
import LabelService from "./LabelService";
|
||||
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
|
||||
import IncidentSeverityService from "./IncidentSeverityService";
|
||||
import {
|
||||
WorkspaceMessageBlock,
|
||||
WorkspacePayloadMarkdown,
|
||||
} from "../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
|
||||
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
|
||||
import { WorkspaceChannel } from "../Utils/Workspace/WorkspaceBase";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -464,6 +475,32 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
|
||||
}
|
||||
}
|
||||
|
||||
// // send message to workspaces - slack, teams, etc.
|
||||
// const createdChannels: {
|
||||
// channelsCreated: Array<WorkspaceChannel>;
|
||||
// } | null = await this.notifyWorkspaceOnIncidentCreate({
|
||||
// projectId: createdItem.projectId,
|
||||
// incidentId: createdItem.id!,
|
||||
// incidentNumber: createdItem.incidentNumber!,
|
||||
// });
|
||||
|
||||
// if (
|
||||
// createdChannels &&
|
||||
// createdChannels.channelsCreated &&
|
||||
// createdChannels.channelsCreated.length > 0
|
||||
// ) {
|
||||
// // update incident with these channels.
|
||||
// await this.updateOneById({
|
||||
// id: createdItem.id!,
|
||||
// data: {
|
||||
// postUpdatesToWorkspaceChannels: createdChannels.channelsCreated,
|
||||
// },
|
||||
// props: {
|
||||
// isRoot: true,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
@@ -644,74 +681,134 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
|
||||
|
||||
if (updatedItemIds.length > 0) {
|
||||
for (const incidentId of updatedItemIds) {
|
||||
let shouldAddIncidentFeed: boolean = false;
|
||||
let feedInfoInMarkdown: string = "**Incident was updated.**";
|
||||
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
if (onUpdate.updateBy.data.title) {
|
||||
// add incident feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
await IncidentFeedService.createIncidentFeed({
|
||||
incidentId: incidentId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Incident title was updated.** Here's the new title.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Title**:
|
||||
${onUpdate.updateBy.data.title || "No title provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
`;
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.rootCause) {
|
||||
// add incident feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
if (onUpdate.updateBy.data.title) {
|
||||
// add incident feed.
|
||||
|
||||
await IncidentFeedService.createIncidentFeed({
|
||||
incidentId: incidentId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Incident root cause was updated.** Here's the new root cause.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Root Cause**:
|
||||
${onUpdate.updateBy.data.rootCause || "No root cause provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
`;
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.description) {
|
||||
// add incident feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
await IncidentFeedService.createIncidentFeed({
|
||||
incidentId: incidentId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Incident description was updated.** Here's the new description.
|
||||
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
feedInfoInMarkdown += `\n\n**Incident Description**:
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`;
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.remediationNotes) {
|
||||
// add incident feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Remediation Notes**:
|
||||
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
|
||||
`;
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.labels &&
|
||||
onUpdate.updateBy.data.labels.length > 0 &&
|
||||
Array.isArray(onUpdate.updateBy.data.labels)
|
||||
) {
|
||||
const labelIds: Array<ObjectID> = (
|
||||
onUpdate.updateBy.data.labels as any
|
||||
)
|
||||
.map((label: Label) => {
|
||||
if (label._id) {
|
||||
return new ObjectID(label._id?.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((labelId: ObjectID | null) => {
|
||||
return labelId !== null;
|
||||
});
|
||||
|
||||
const labels: Array<Label> = await LabelService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(labelIds),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (labels.length > 0) {
|
||||
feedInfoInMarkdown += `\n\n**Labels**:
|
||||
|
||||
${labels
|
||||
.map((label: Label) => {
|
||||
return `- ${label.name}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.incidentSeverity &&
|
||||
(onUpdate.updateBy.data.incidentSeverity as any)._id
|
||||
) {
|
||||
const incidentSeverity: IncidentSeverity | null =
|
||||
await IncidentSeverityService.findOneBy({
|
||||
query: {
|
||||
_id: new ObjectID(
|
||||
(
|
||||
onUpdate.updateBy.data.incidentSeverity as any
|
||||
)?._id.toString(),
|
||||
),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (incidentSeverity) {
|
||||
feedInfoInMarkdown += `\n\n**Incident Severity**:
|
||||
${incidentSeverity.name}
|
||||
`;
|
||||
|
||||
shouldAddIncidentFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAddIncidentFeed) {
|
||||
await IncidentFeedService.createIncidentFeed({
|
||||
incidentId: incidentId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Remediation notes were updated.** Here are the new notes.
|
||||
|
||||
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
|
||||
`,
|
||||
feedInfoInMarkdown: feedInfoInMarkdown,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
}
|
||||
@@ -1250,5 +1347,131 @@ ${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
public async notifyWorkspaceOnIncidentCreate(data: {
|
||||
projectId: ObjectID;
|
||||
incidentId: ObjectID;
|
||||
incidentNumber: number;
|
||||
}): Promise<{
|
||||
channelsCreated: WorkspaceChannel[];
|
||||
} | null> {
|
||||
try {
|
||||
// we will notify the workspace about the incident creation with the bot tokken which is in WorkspaceProjectAuth Table.
|
||||
return await WorkspaceNotificationRuleService.createInviteAndPostToChannelsBasedOnRules(
|
||||
{
|
||||
projectId: data.projectId,
|
||||
notificationFor: {
|
||||
incidentId: data.incidentId,
|
||||
},
|
||||
notificationRuleEventType: NotificationRuleEventType.Incident,
|
||||
channelNameSiffix: data.incidentNumber.toString(),
|
||||
messageBlocks: await this.getWorkspaceMessageBlocksForIncidentCreate({
|
||||
incidentId: data.incidentId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
// log the error and continue.
|
||||
logger.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getWorkspaceMessageBlocksForIncidentCreate(data: {
|
||||
incidentId: ObjectID;
|
||||
}): Promise<Array<WorkspaceMessageBlock>> {
|
||||
const incident: Model | null = await this.findOneById({
|
||||
id: data.incidentId,
|
||||
select: {
|
||||
projectId: true,
|
||||
incidentNumber: true,
|
||||
title: true,
|
||||
description: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
},
|
||||
rootCause: true,
|
||||
remediationNotes: true,
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident) {
|
||||
throw new BadDataException("Incident not found");
|
||||
}
|
||||
|
||||
const blocks: Array<WorkspaceMessageBlock> = [];
|
||||
|
||||
if (incident.incidentNumber) {
|
||||
const markdownBlock1: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Incident #${incident.incidentNumber} Created**`,
|
||||
};
|
||||
blocks.push(markdownBlock1);
|
||||
}
|
||||
|
||||
if (incident.title) {
|
||||
const markdownBlock2: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Incident Title**:
|
||||
${incident.title}`,
|
||||
};
|
||||
blocks.push(markdownBlock2);
|
||||
}
|
||||
|
||||
if (incident.description) {
|
||||
const markdownBlock3: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Description**:
|
||||
${incident.description}`,
|
||||
};
|
||||
blocks.push(markdownBlock3);
|
||||
}
|
||||
|
||||
if (incident.incidentSeverity?.name) {
|
||||
const markdownBlock4: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Severity**:
|
||||
${incident.incidentSeverity.name}`,
|
||||
};
|
||||
blocks.push(markdownBlock4);
|
||||
}
|
||||
|
||||
if (incident.rootCause) {
|
||||
const markdownBlock5: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Root Cause**:
|
||||
${incident.rootCause}`,
|
||||
};
|
||||
blocks.push(markdownBlock5);
|
||||
}
|
||||
|
||||
if (incident.remediationNotes) {
|
||||
const markdownBlock6: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Remediation Notes**:
|
||||
${incident.remediationNotes}`,
|
||||
};
|
||||
blocks.push(markdownBlock6);
|
||||
}
|
||||
|
||||
if (incident.currentIncidentState?.name) {
|
||||
const markdownBlock7: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `**Incident State**:
|
||||
${incident.currentIncidentState.name}`,
|
||||
};
|
||||
blocks.push(markdownBlock7);
|
||||
}
|
||||
|
||||
// TODO: Add buttons to Post Private Note, Ack Incident, Resolve Incident. etc.
|
||||
|
||||
return blocks as Array<WorkspaceMessageBlock>;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
@@ -151,6 +151,12 @@ import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
|
||||
import AlertFeedService from "./AlertFeedService";
|
||||
import IncidentFeedService from "./IncidentFeedService";
|
||||
|
||||
import MonitorTestService from "./MonitorTestService";
|
||||
import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
|
||||
import WorkspaceSettingService from "./WorkspaceSettingService";
|
||||
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
|
||||
|
||||
const services: Array<BaseService> = [
|
||||
AcmeCertificateService,
|
||||
PromoCodeService,
|
||||
@@ -314,6 +320,12 @@ const services: Array<BaseService> = [
|
||||
AlertFeedService,
|
||||
|
||||
TableViewService,
|
||||
MonitorTestService,
|
||||
|
||||
WorkspaceProjectAuthTokenService,
|
||||
WorkspaceUserAuthTokenService,
|
||||
WorkspaceSettingService,
|
||||
WorkspaceNotificationRuleService,
|
||||
];
|
||||
|
||||
export const AnalyticsServices: Array<
|
||||
|
||||
@@ -52,6 +52,7 @@ import NotificationSettingEventType from "../../Types/NotificationSetting/Notifi
|
||||
import Query from "../Types/Database/Query";
|
||||
import DeleteBy from "../Types/Database/DeleteBy";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -643,6 +644,48 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
public async getLabelsForMonitors(data: {
|
||||
monitorIds: Array<ObjectID>;
|
||||
}): Promise<Array<Label>> {
|
||||
if (data.monitorIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const monitors: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(data.monitorIds),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
labels: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
});
|
||||
|
||||
const labels: Array<Label> = [];
|
||||
|
||||
for (const monitor of monitors) {
|
||||
if (monitor.labels) {
|
||||
for (const label of monitor.labels) {
|
||||
const isLabelAlreadyAdded: boolean = labels.some((l: Label) => {
|
||||
return l.id!.toString() === label.id!.toString();
|
||||
});
|
||||
|
||||
if (!isLabelAlreadyAdded) {
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
public async notifyOwnersWhenNoProbeIsEnabled(data: {
|
||||
monitorId: ObjectID;
|
||||
isNoProbesEnabled: boolean;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import ResellerPlan from "Common/Models/DatabaseModels/ResellerPlan";
|
||||
import { IsBillingEnabled, getAllEnvVars } from "../EnvironmentConfig";
|
||||
import {
|
||||
IsBillingEnabled,
|
||||
NotificationSlackWebhookOnCreateProject,
|
||||
NotificationSlackWebhookOnDeleteProject,
|
||||
NotificationSlackWebhookOnSubscriptionUpdate,
|
||||
getAllEnvVars,
|
||||
} from "../EnvironmentConfig";
|
||||
import AllMeteredPlans from "../Types/Billing/MeteredPlan/AllMeteredPlans";
|
||||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import DeleteBy from "../Types/Database/DeleteBy";
|
||||
@@ -61,6 +67,9 @@ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
||||
import AlertSeverityService from "./AlertSeverityService";
|
||||
import AlertState from "../../Models/DatabaseModels/AlertState";
|
||||
import AlertStateService from "./AlertStateService";
|
||||
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
||||
import URL from "../../Types/API/URL";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
|
||||
export interface CurrentPlan {
|
||||
plan: PlanType | null;
|
||||
@@ -95,6 +104,8 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug("Creating project for user " + data.props.userId);
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: data.props.userId,
|
||||
select: {
|
||||
@@ -398,6 +409,13 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
project.paymentProviderSubscriptionSeats +
|
||||
" completed and project updated.",
|
||||
);
|
||||
|
||||
if (project.id) {
|
||||
// send slack message on plan change.
|
||||
await this.sendSubscriptionChangeWebhookSlackNotification(
|
||||
project.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,6 +423,57 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
return { updateBy, carryForward: [] };
|
||||
}
|
||||
|
||||
private async sendSubscriptionChangeWebhookSlackNotification(
|
||||
projectId: ObjectID,
|
||||
): Promise<void> {
|
||||
if (NotificationSlackWebhookOnSubscriptionUpdate) {
|
||||
// fetch project again.
|
||||
const project: Model | null = await this.findOneById({
|
||||
id: new ObjectID(projectId.toString()),
|
||||
select: {
|
||||
name: true,
|
||||
_id: true,
|
||||
createdOwnerName: true,
|
||||
createdOwnerEmail: true,
|
||||
planName: true,
|
||||
createdByUserId: true,
|
||||
paymentProviderSubscriptionStatus: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
let slackMessage: string = `*Project Plan Changed:*
|
||||
*Project Name:* ${project.name?.toString() || "N/A"}
|
||||
*Project ID:* ${project.id?.toString() || "N/A"}
|
||||
`;
|
||||
|
||||
if (project.createdOwnerName && project.createdOwnerEmail) {
|
||||
slackMessage += `*Project Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
|
||||
`;
|
||||
}
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
// which plan?
|
||||
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
|
||||
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
|
||||
`;
|
||||
}
|
||||
|
||||
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(NotificationSlackWebhookOnSubscriptionUpdate),
|
||||
text: slackMessage,
|
||||
}).catch((error: Exception) => {
|
||||
logger.error("Error sending slack message: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async addDefaultScheduledMaintenanceState(
|
||||
createdItem: Model,
|
||||
): Promise<Model> {
|
||||
@@ -561,6 +630,53 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
createdItem = await this.addDefaultScheduledMaintenanceState(createdItem);
|
||||
createdItem = await this.addDefaultAlertState(createdItem);
|
||||
|
||||
if (NotificationSlackWebhookOnCreateProject) {
|
||||
// fetch project again.
|
||||
const project: Model | null = await this.findOneById({
|
||||
id: createdItem.id!,
|
||||
select: {
|
||||
name: true,
|
||||
_id: true,
|
||||
createdOwnerName: true,
|
||||
createdOwnerEmail: true,
|
||||
planName: true,
|
||||
createdByUserId: true,
|
||||
paymentProviderSubscriptionStatus: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
let slackMessage: string = `*Project Created:*
|
||||
*Project Name:* ${project.name?.toString() || "N/A"}
|
||||
*Project ID:* ${project.id?.toString() || "N/A"}
|
||||
`;
|
||||
|
||||
if (project.createdOwnerName && project.createdOwnerEmail) {
|
||||
slackMessage += `*Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
|
||||
`;
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
// which plan?
|
||||
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
|
||||
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
|
||||
`;
|
||||
}
|
||||
|
||||
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(NotificationSlackWebhookOnCreateProject),
|
||||
text: slackMessage,
|
||||
}).catch((error: Exception) => {
|
||||
logger.error("Error sending slack message: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
@@ -1007,29 +1123,71 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
protected override async onBeforeDelete(
|
||||
deleteBy: DeleteBy<Model>,
|
||||
): Promise<OnDelete<Model>> {
|
||||
if (IsBillingEnabled) {
|
||||
const projects: Array<Model> = await this.findBy({
|
||||
query: deleteBy.query,
|
||||
props: deleteBy.props,
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
paymentProviderSubscriptionId: true,
|
||||
paymentProviderMeteredSubscriptionId: true,
|
||||
const projects: Array<Model> = await this.findBy({
|
||||
query: deleteBy.query,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
paymentProviderSubscriptionId: true,
|
||||
paymentProviderMeteredSubscriptionId: true,
|
||||
name: true,
|
||||
createdByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { deleteBy, carryForward: projects };
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: [] };
|
||||
return { deleteBy, carryForward: projects };
|
||||
}
|
||||
|
||||
protected override async onDeleteSuccess(
|
||||
onDelete: OnDelete<Model>,
|
||||
_itemIdsBeforeDelete: ObjectID[],
|
||||
): Promise<OnDelete<Model>> {
|
||||
if (NotificationSlackWebhookOnDeleteProject) {
|
||||
for (const project of onDelete.carryForward) {
|
||||
let subscriptionStatus: SubscriptionStatus | null = null;
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
subscriptionStatus = await BillingService.getSubscriptionStatus(
|
||||
project.paymentProviderSubscriptionId!,
|
||||
);
|
||||
}
|
||||
|
||||
let slackMessage: string = `*Project Deleted:*
|
||||
*Project Name:* ${project.name?.toString() || "N/A"}
|
||||
*Project ID:* ${project._id?.toString() || "N/A"}
|
||||
`;
|
||||
|
||||
if (subscriptionStatus) {
|
||||
slackMessage += `*Project Subscription Status:* ${subscriptionStatus?.toString() || "N/A"}
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
project.createdByUser &&
|
||||
project.createdByUser.name &&
|
||||
project.createdByUser.email
|
||||
) {
|
||||
slackMessage += `*Created By:* ${project?.createdByUser.name?.toString() + " (" + project.createdByUser.email.toString() + ")" || "N/A"}
|
||||
`;
|
||||
}
|
||||
|
||||
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(NotificationSlackWebhookOnDeleteProject),
|
||||
text: slackMessage,
|
||||
}).catch((err: Error) => {
|
||||
// log this error but do not throw it. Not important enough to stop the process.
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// get project id
|
||||
if (IsBillingEnabled) {
|
||||
for (const project of onDelete.carryForward) {
|
||||
@@ -1224,6 +1382,9 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// send slack message on plan change.
|
||||
await this.sendSubscriptionChangeWebhookSlackNotification(projectId);
|
||||
}
|
||||
|
||||
public getActiveProjectStatusQuery(): Query<Model> {
|
||||
|
||||
@@ -50,6 +50,8 @@ import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
||||
import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
|
||||
import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
|
||||
import { Gray500, Red500 } from "../../Types/BrandColors";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
import LabelService from "./LabelService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -59,6 +61,32 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingScheduledMaintenanceNumberForProject(data: {
|
||||
projectId: ObjectID;
|
||||
}): Promise<number> {
|
||||
// get last scheduledMaintenance number.
|
||||
const lastScheduledMaintenance: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
},
|
||||
select: {
|
||||
scheduledMaintenanceNumber: true,
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!lastScheduledMaintenance) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return lastScheduledMaintenance.scheduledMaintenanceNumber || 0;
|
||||
}
|
||||
|
||||
public async notififySubscribersOnEventScheduled(
|
||||
scheduledEvents: Array<Model>,
|
||||
): Promise<void> {
|
||||
@@ -233,6 +261,8 @@ export class Service extends DatabaseService<Model> {
|
||||
isPublicStatusPage: statuspage.isPublicStatusPage
|
||||
? "true"
|
||||
: "false",
|
||||
subscriberEmailNotificationFooterText:
|
||||
statuspage.subscriberEmailNotificationFooterText || "",
|
||||
resourcesAffected: resourcesAffected,
|
||||
scheduledAt:
|
||||
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
||||
@@ -391,10 +421,13 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
const projectId: ObjectID =
|
||||
createBy.props.tenantId || createBy.data.projectId!;
|
||||
|
||||
const scheduledMaintenanceState: ScheduledMaintenanceState | null =
|
||||
await ScheduledMaintenanceStateService.findOneBy({
|
||||
query: {
|
||||
projectId: createBy.props.tenantId,
|
||||
projectId: projectId,
|
||||
isScheduledState: true,
|
||||
},
|
||||
select: {
|
||||
@@ -414,6 +447,14 @@ export class Service extends DatabaseService<Model> {
|
||||
createBy.data.currentScheduledMaintenanceStateId =
|
||||
scheduledMaintenanceState.id;
|
||||
|
||||
const scheduledMaintenanceNumberForThisScheduledMaintenance: number =
|
||||
(await this.getExistingScheduledMaintenanceNumberForProject({
|
||||
projectId: projectId,
|
||||
})) + 1;
|
||||
|
||||
createBy.data.scheduledMaintenanceNumber =
|
||||
scheduledMaintenanceNumberForThisScheduledMaintenance;
|
||||
|
||||
// get next notification date.
|
||||
|
||||
if (
|
||||
@@ -451,7 +492,7 @@ export class Service extends DatabaseService<Model> {
|
||||
scheduledMaintenanceFeedEventType:
|
||||
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceCreated,
|
||||
displayColor: Red500,
|
||||
feedInfoInMarkdown: `**Scheduled Maintenance Created**:
|
||||
feedInfoInMarkdown: `**Scheduled Maintenance #${createdItem.scheduledMaintenanceNumber?.toString()} Created**:
|
||||
|
||||
**Scheduled Maintenance Title**:
|
||||
|
||||
@@ -692,40 +733,219 @@ ${createdItem.description || "No description provided."}
|
||||
|
||||
if (updatedItemIds.length > 0) {
|
||||
for (const scheduledMaintenanceId of updatedItemIds) {
|
||||
let shouldAddScheduledMaintenanceFeed: boolean = false;
|
||||
let feedInfoInMarkdown: string =
|
||||
"**Scheduled Maintenance was updated.**";
|
||||
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
if (onUpdate.updateBy.data.title) {
|
||||
// add scheduledMaintenance feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeed({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
scheduledMaintenanceFeedEventType:
|
||||
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Scheduled Maintenance title was updated.** Here's the new title.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Title**:
|
||||
${onUpdate.updateBy.data.title || "No title provided."}
|
||||
`,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
`;
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.startsAt) {
|
||||
// add scheduledMaintenance feed.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Starts At**:
|
||||
${OneUptimeDate.getDateAsLocalFormattedString(onUpdate.updateBy.data.startsAt as Date) || "No title provided."}
|
||||
`;
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.endsAt) {
|
||||
// add scheduledMaintenance feed.
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Ends At**:
|
||||
${OneUptimeDate.getDateAsLocalFormattedString(onUpdate.updateBy.data.endsAt as Date) || "No title provided."}
|
||||
`;
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
|
||||
if (onUpdate.updateBy.data.description) {
|
||||
// add scheduledMaintenance feed.
|
||||
const createdByUserId: ObjectID | undefined | null =
|
||||
onUpdate.updateBy.props.userId;
|
||||
|
||||
feedInfoInMarkdown += `\n\n**Scheduled Maintenance Description**:
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`;
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent &&
|
||||
Array.isArray(
|
||||
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent,
|
||||
) &&
|
||||
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent
|
||||
.length > 0
|
||||
) {
|
||||
feedInfoInMarkdown += `\n\n**Notify Subscribers Before Event Starts**:
|
||||
${(
|
||||
onUpdate.updateBy.data
|
||||
.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>
|
||||
)
|
||||
.map((recurring: Recurring) => {
|
||||
return `- ${(recurring as Recurring).toString()}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.monitors &&
|
||||
onUpdate.updateBy.data.monitors.length > 0 &&
|
||||
Array.isArray(onUpdate.updateBy.data.monitors)
|
||||
) {
|
||||
const monitorIds: Array<ObjectID> = (
|
||||
onUpdate.updateBy.data.monitors as any
|
||||
)
|
||||
.map((monitor: Label) => {
|
||||
if (monitor._id) {
|
||||
return new ObjectID(monitor._id?.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((monitorId: ObjectID | null) => {
|
||||
return monitorId !== null;
|
||||
});
|
||||
|
||||
const monitors: Array<Label> = await MonitorService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(monitorIds),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (monitors.length > 0) {
|
||||
feedInfoInMarkdown += `\n\n**Resources Affected**:
|
||||
|
||||
${monitors
|
||||
.map((monitor: Monitor) => {
|
||||
return `- ${monitor.name}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.statusPages &&
|
||||
onUpdate.updateBy.data.statusPages.length > 0 &&
|
||||
Array.isArray(onUpdate.updateBy.data.statusPages)
|
||||
) {
|
||||
const statusPageIds: Array<ObjectID> = (
|
||||
onUpdate.updateBy.data.statusPages as any
|
||||
)
|
||||
.map((statusPage: Label) => {
|
||||
if (statusPage._id) {
|
||||
return new ObjectID(statusPage._id?.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((statusPageId: ObjectID | null) => {
|
||||
return statusPageId !== null;
|
||||
});
|
||||
|
||||
const statusPages: Array<Label> = await StatusPageService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(statusPageIds),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (statusPages.length > 0) {
|
||||
feedInfoInMarkdown += `\n\n**Show on these status pages:**:
|
||||
|
||||
${statusPages
|
||||
.map((statusPage: StatusPage) => {
|
||||
return `- ${statusPage.name}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
onUpdate.updateBy.data.labels &&
|
||||
onUpdate.updateBy.data.labels.length > 0 &&
|
||||
Array.isArray(onUpdate.updateBy.data.labels)
|
||||
) {
|
||||
const labelIds: Array<ObjectID> = (
|
||||
onUpdate.updateBy.data.labels as any
|
||||
)
|
||||
.map((label: Label) => {
|
||||
if (label._id) {
|
||||
return new ObjectID(label._id?.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((labelId: ObjectID | null) => {
|
||||
return labelId !== null;
|
||||
});
|
||||
|
||||
const labels: Array<Label> = await LabelService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(labelIds),
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (labels.length > 0) {
|
||||
feedInfoInMarkdown += `\n\n**Labels**:
|
||||
|
||||
${labels
|
||||
.map((label: Label) => {
|
||||
return `- ${label.name}`;
|
||||
})
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
shouldAddScheduledMaintenanceFeed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAddScheduledMaintenanceFeed) {
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeed({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
||||
scheduledMaintenanceFeedEventType:
|
||||
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceUpdated,
|
||||
displayColor: Gray500,
|
||||
feedInfoInMarkdown: `**Scheduled Maintenance description was updated.** Here's the new description.
|
||||
|
||||
${onUpdate.updateBy.data.description || "No description provided."}
|
||||
`,
|
||||
feedInfoInMarkdown: feedInfoInMarkdown,
|
||||
userId: createdByUserId || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -148,6 +148,12 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
createBy.data.defaultBarColor = Green;
|
||||
}
|
||||
|
||||
if (!createBy.data.subscriberEmailNotificationFooterText) {
|
||||
createBy.data.subscriberEmailNotificationFooterText =
|
||||
"This is an automated email sent to you because you are subscribed to " +
|
||||
createBy.data.name;
|
||||
}
|
||||
|
||||
return {
|
||||
createBy,
|
||||
carryForward: null,
|
||||
@@ -469,7 +475,9 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
},
|
||||
});
|
||||
|
||||
let statusPageURL: string = domain?.fullDomain || "";
|
||||
let statusPageURL: string = domain?.fullDomain
|
||||
? `https://${domain.fullDomain}`
|
||||
: "";
|
||||
|
||||
if (!statusPageURL) {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
@@ -645,6 +653,8 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
templateType: EmailTemplateType.StatusPageSubscriberReport,
|
||||
vars: {
|
||||
statusPageName: statusPageName,
|
||||
subscriberEmailNotificationFooterText:
|
||||
statuspage.subscriberEmailNotificationFooterText || "",
|
||||
statusPageUrl: statusPageURL,
|
||||
hasResources: report.totalResources > 0 ? "true" : "false",
|
||||
report: report as any,
|
||||
|
||||
@@ -356,6 +356,11 @@ export class Service extends DatabaseService<Model> {
|
||||
subscriber.subscriberEmail &&
|
||||
subscriber._id
|
||||
) {
|
||||
const unsubscribeUrl: string = this.getUnsubscribeLink(
|
||||
URL.fromString(statusPageURL),
|
||||
subscriber.id!,
|
||||
).toString();
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
toEmail: subscriber.subscriberEmail,
|
||||
@@ -373,6 +378,7 @@ export class Service extends DatabaseService<Model> {
|
||||
? "true"
|
||||
: "false",
|
||||
confirmationUrl: confirmSubscriptionLink,
|
||||
unsubscribeUrl: unsubscribeUrl,
|
||||
},
|
||||
subject: "Confirm your subscription to " + statusPageName,
|
||||
},
|
||||
@@ -630,6 +636,7 @@ export class Service extends DatabaseService<Model> {
|
||||
isPublicStatusPage: true,
|
||||
logoFileId: true,
|
||||
allowSubscribersToChooseResources: true,
|
||||
subscriberEmailNotificationFooterText: true,
|
||||
allowSubscribersToChooseEventTypes: true,
|
||||
smtpConfig: {
|
||||
_id: true,
|
||||
|
||||
@@ -90,6 +90,7 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
isNewUser = true;
|
||||
user = await UserService.createByEmail({
|
||||
email,
|
||||
name: undefined, // name is not required for now.
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DatabaseConfig from "../DatabaseConfig";
|
||||
import {
|
||||
IsBillingEnabled,
|
||||
NotificationWebhookOnCreateUser,
|
||||
NotificationSlackWebhookOnCreateUser,
|
||||
} from "../EnvironmentConfig";
|
||||
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
||||
import UpdateBy from "../Types/Database/UpdateBy";
|
||||
@@ -28,10 +28,11 @@ import Text from "../../Types/Text";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import Model from "Common/Models/DatabaseModels/User";
|
||||
import SlackUtil from "../Utils/Slack";
|
||||
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTwoFactorAuthService from "./UserTwoFactorAuthService";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Name from "../../Types/Name";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -42,9 +43,9 @@ export class Service extends DatabaseService<Model> {
|
||||
_onCreate: OnCreate<Model>,
|
||||
createdItem: Model,
|
||||
): Promise<Model> {
|
||||
if (NotificationWebhookOnCreateUser) {
|
||||
SlackUtil.sendMessageToChannel({
|
||||
url: URL.fromString(NotificationWebhookOnCreateUser),
|
||||
if (NotificationSlackWebhookOnCreateUser) {
|
||||
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(NotificationSlackWebhookOnCreateUser),
|
||||
text: `*New OneUptime User:*
|
||||
*Email:* ${createdItem.email?.toString() || "N/A"}
|
||||
*Name:* ${createdItem.name?.toString() || "N/A"}
|
||||
@@ -292,6 +293,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
public async createByEmail(data: {
|
||||
email: Email;
|
||||
name: Name | undefined;
|
||||
isEmailVerified?: boolean;
|
||||
generateRandomPassword?: boolean;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
@@ -300,6 +302,9 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
const user: Model = new Model();
|
||||
user.email = email;
|
||||
if (data.name) {
|
||||
user.name = data.name;
|
||||
}
|
||||
user.isEmailVerified = data.isEmailVerified || false;
|
||||
|
||||
if (data.generateRandomPassword) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
if (IsBillingEnabled) {
|
||||
this.hardDeleteItemsOlderThanInDays("createdAt", 3);
|
||||
this.hardDeleteItemsOlderThanInDays("createdAt", 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
711
Common/Server/Services/WorkspaceNotificationRuleService.ts
Normal file
711
Common/Server/Services/WorkspaceNotificationRuleService.ts
Normal file
@@ -0,0 +1,711 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "Common/Models/DatabaseModels/WorkspaceNotificationRule";
|
||||
import IncidentNotificationRule from "../../Types/Workspace/NotificationRules/NotificationRuleTypes/IncidentNotificationRule";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import IncidentService from "./IncidentService";
|
||||
import { NotificationRuleConditionCheckOn } from "../../Types/Workspace/NotificationRules/NotificationRuleCondition";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
import MonitorService from "./MonitorService";
|
||||
import Alert from "../../Models/DatabaseModels/Alert";
|
||||
import AlertService from "./AlertService";
|
||||
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
||||
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
||||
import MonitorStatusTimeline from "Common/Models/DatabaseModels/MonitorStatusTimeline";
|
||||
import MonitorStatusTimelineService from "./MonitorStatusTimelineService";
|
||||
import { WorkspaceNotificationRuleUtil } from "../../Types/Workspace/NotificationRules/NotificationRuleUtil";
|
||||
import TeamMemberService from "./TeamMemberService";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import BaseNotificationRule from "../../Types/Workspace/NotificationRules/BaseNotificationRule";
|
||||
import CreateChannelNotificationRule from "../../Types/Workspace/NotificationRules/CreateChannelNotificationRule";
|
||||
import { WorkspaceChannel } from "../Utils/Workspace/WorkspaceBase";
|
||||
import WorkspaceUtil from "../Utils/Workspace/Workspace";
|
||||
import WorkspaceUserAuthToken from "../../Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
|
||||
import WorkspaceMessagePayload, {
|
||||
WorkspaceMessageBlock,
|
||||
} from "../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import WorkspaceProjectAuthToken from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
|
||||
|
||||
export interface NotificationFor {
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
monitorStatusTimelineId?: ObjectID | undefined;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async createInviteAndPostToChannelsBasedOnRules(data: {
|
||||
projectId: ObjectID;
|
||||
notificationRuleEventType: NotificationRuleEventType;
|
||||
notificationFor: NotificationFor;
|
||||
channelNameSiffix: string;
|
||||
messageBlocks: Array<WorkspaceMessageBlock>;
|
||||
}): Promise<{
|
||||
channelsCreated: Array<WorkspaceChannel>;
|
||||
} | null> {
|
||||
const channelsCreated: Array<WorkspaceChannel> = [];
|
||||
|
||||
const projectAuths: Array<WorkspaceProjectAuthToken> =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuths({
|
||||
projectId: data.projectId,
|
||||
});
|
||||
|
||||
if (!projectAuths || projectAuths.length === 0) {
|
||||
// do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const projectAuth of projectAuths) {
|
||||
if (!projectAuth.authToken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!projectAuth.workspaceType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const authToken: string = projectAuth.authToken;
|
||||
const workspaceType: WorkspaceType = projectAuth.workspaceType;
|
||||
|
||||
const notificationRules: Array<Model> =
|
||||
await this.getMatchingNotificationRules({
|
||||
projectId: data.projectId,
|
||||
workspaceType: workspaceType,
|
||||
notificationRuleEventType: data.notificationRuleEventType,
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
if (!notificationRules || notificationRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createdWorkspaceChannels: Array<WorkspaceChannel> =
|
||||
await this.createChannelsBasedOnRules({
|
||||
projectOrUserAuthTokenForWorkspasce: authToken,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules.map((rule: Model) => {
|
||||
return rule.notificationRule as CreateChannelNotificationRule;
|
||||
}),
|
||||
channelNameSiffix: data.channelNameSiffix,
|
||||
notificationEventType: data.notificationRuleEventType,
|
||||
});
|
||||
|
||||
await this.inviteUsersAndTeamsToChannelsBasedOnRules({
|
||||
projectId: data.projectId,
|
||||
projectOrUserAuthTokenForWorkspasce: authToken,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules.map((rule: Model) => {
|
||||
return rule.notificationRule as CreateChannelNotificationRule;
|
||||
}),
|
||||
channelNames: createdWorkspaceChannels.map(
|
||||
(channel: WorkspaceChannel) => {
|
||||
return channel.name;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const existingChannelNames: Array<string> =
|
||||
this.getExistingChannelNamesFromNotificationRules({
|
||||
notificationRules: notificationRules.map((rule: Model) => {
|
||||
return rule.notificationRule as BaseNotificationRule;
|
||||
}),
|
||||
}) || [];
|
||||
|
||||
// add created channel names to existing channel names.
|
||||
for (const channel of createdWorkspaceChannels) {
|
||||
if (!existingChannelNames.includes(channel.name)) {
|
||||
existingChannelNames.push(channel.name);
|
||||
}
|
||||
}
|
||||
|
||||
await this.postToWorkspaceChannels({
|
||||
projectOrUserAuthTokenForWorkspasce: authToken,
|
||||
workspaceType: workspaceType,
|
||||
workspaceMessagePayload: {
|
||||
_type: "WorkspaceMessagePayload",
|
||||
channelNames: existingChannelNames,
|
||||
messageBlocks: data.messageBlocks,
|
||||
},
|
||||
});
|
||||
|
||||
channelsCreated.push(...createdWorkspaceChannels);
|
||||
}
|
||||
|
||||
return {
|
||||
channelsCreated: channelsCreated,
|
||||
};
|
||||
}
|
||||
|
||||
public async postToWorkspaceChannels(data: {
|
||||
projectOrUserAuthTokenForWorkspasce: string;
|
||||
workspaceType: WorkspaceType;
|
||||
workspaceMessagePayload: WorkspaceMessagePayload;
|
||||
}): Promise<void> {
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(data.workspaceType).sendMessage({
|
||||
workspaceMessagePayload: data.workspaceMessagePayload,
|
||||
authToken: data.projectOrUserAuthTokenForWorkspasce,
|
||||
});
|
||||
}
|
||||
|
||||
public async inviteUsersAndTeamsToChannelsBasedOnRules(data: {
|
||||
projectId: ObjectID;
|
||||
projectOrUserAuthTokenForWorkspasce: string;
|
||||
workspaceType: WorkspaceType;
|
||||
notificationRules: Array<CreateChannelNotificationRule>;
|
||||
channelNames: Array<string>;
|
||||
}): Promise<void> {
|
||||
const inviteUserIds: Array<ObjectID> =
|
||||
await this.getUsersIdsToInviteToChannel({
|
||||
notificationRules: data.notificationRules,
|
||||
});
|
||||
|
||||
const workspaceUserIds: Array<string> = [];
|
||||
|
||||
for (const userId of inviteUserIds) {
|
||||
const workspaceUserId: string | null =
|
||||
await this.getWorkspaceUserIdFromOneUptimeUserId({
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
oneupitmeUserId: userId,
|
||||
});
|
||||
|
||||
if (workspaceUserId) {
|
||||
workspaceUserIds.push(workspaceUserId);
|
||||
}
|
||||
}
|
||||
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType,
|
||||
).inviteUsersToChannels({
|
||||
authToken: data.projectOrUserAuthTokenForWorkspasce,
|
||||
workspaceChannelInvitationPayload: {
|
||||
channelNames: data.channelNames,
|
||||
workspaceUserIds: workspaceUserIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getWorkspaceUserIdFromOneUptimeUserId(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
oneupitmeUserId: ObjectID;
|
||||
}): Promise<string | null> {
|
||||
const userAuth: WorkspaceUserAuthToken | null =
|
||||
await WorkspaceUserAuthTokenService.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
userId: data.oneupitmeUserId,
|
||||
},
|
||||
select: {
|
||||
workspaceUserId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return userAuth.workspaceUserId?.toString() || null;
|
||||
}
|
||||
|
||||
public async createChannelsBasedOnRules(data: {
|
||||
projectOrUserAuthTokenForWorkspasce: string;
|
||||
workspaceType: WorkspaceType;
|
||||
notificationRules: Array<CreateChannelNotificationRule>;
|
||||
channelNameSiffix: string;
|
||||
notificationEventType: NotificationRuleEventType;
|
||||
}): Promise<Array<WorkspaceChannel>> {
|
||||
const createdWorkspaceChannels: Array<WorkspaceChannel> = [];
|
||||
const createdChannelNames: Array<string> = [];
|
||||
|
||||
const newChannelNames: Array<string> =
|
||||
this.getNewChannelNamesFromNotificationRules({
|
||||
notificationRules: data.notificationRules,
|
||||
channelNameSiffix: data.channelNameSiffix,
|
||||
notificationEventType: data.notificationEventType,
|
||||
});
|
||||
|
||||
if (!newChannelNames || newChannelNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const newChannelName of newChannelNames) {
|
||||
// if already created then skip it.
|
||||
if (createdChannelNames.includes(newChannelName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// create channel.
|
||||
const channel: WorkspaceChannel =
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType,
|
||||
).createChannel({
|
||||
authToken: data.projectOrUserAuthTokenForWorkspasce,
|
||||
channelName: newChannelName,
|
||||
});
|
||||
|
||||
createdChannelNames.push(channel.name);
|
||||
|
||||
createdWorkspaceChannels.push(channel);
|
||||
}
|
||||
|
||||
return createdWorkspaceChannels;
|
||||
}
|
||||
|
||||
public async getUsersIdsToInviteToChannel(data: {
|
||||
notificationRules: Array<CreateChannelNotificationRule>;
|
||||
}): Promise<Array<ObjectID>> {
|
||||
const inviteUserIds: Array<ObjectID> = [];
|
||||
|
||||
for (const notificationRule of data.notificationRules) {
|
||||
const workspaceRules: CreateChannelNotificationRule = notificationRule;
|
||||
|
||||
if (workspaceRules.shouldCreateNewChannel) {
|
||||
if (
|
||||
workspaceRules.inviteUsersToNewChannel &&
|
||||
workspaceRules.inviteUsersToNewChannel.length > 0
|
||||
) {
|
||||
const userIds: Array<ObjectID> =
|
||||
workspaceRules.inviteUsersToNewChannel || [];
|
||||
|
||||
for (const userId of userIds) {
|
||||
if (
|
||||
!inviteUserIds.find((id: ObjectID) => {
|
||||
return id.toString() === userId.toString();
|
||||
})
|
||||
) {
|
||||
inviteUserIds.push(new ObjectID(userId.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
workspaceRules.inviteTeamsToNewChannel &&
|
||||
workspaceRules.inviteTeamsToNewChannel.length > 0
|
||||
) {
|
||||
let teamIds: Array<ObjectID> =
|
||||
workspaceRules.inviteTeamsToNewChannel || [];
|
||||
|
||||
teamIds = teamIds.map((teamId: ObjectID) => {
|
||||
return new ObjectID(teamId.toString());
|
||||
});
|
||||
|
||||
const usersInTeam: Array<User> =
|
||||
await TeamMemberService.getUsersInTeams(teamIds);
|
||||
|
||||
for (const user of usersInTeam) {
|
||||
if (
|
||||
!inviteUserIds.find((id: ObjectID) => {
|
||||
return id.toString() === user._id?.toString();
|
||||
})
|
||||
) {
|
||||
const userId: string | undefined = user._id?.toString();
|
||||
if (userId) {
|
||||
inviteUserIds.push(new ObjectID(userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inviteUserIds;
|
||||
}
|
||||
|
||||
public getExistingChannelNamesFromNotificationRules(data: {
|
||||
notificationRules: Array<BaseNotificationRule>;
|
||||
}): Array<string> {
|
||||
const channelNames: Array<string> = [];
|
||||
|
||||
for (const notificationRule of data.notificationRules) {
|
||||
const workspaceRules: BaseNotificationRule = notificationRule;
|
||||
|
||||
if (workspaceRules.shouldPostToExistingChannel) {
|
||||
const existingChannelNames: Array<string> =
|
||||
workspaceRules.existingChannelNames.split(",");
|
||||
|
||||
for (const channelName of existingChannelNames) {
|
||||
if (!channelName) {
|
||||
// if channel name is empty then skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!channelNames.includes(channelName)) {
|
||||
// if channel name is not already added then add it.
|
||||
channelNames.push(channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channelNames;
|
||||
}
|
||||
|
||||
public getNewChannelNamesFromNotificationRules(data: {
|
||||
notificationEventType: NotificationRuleEventType;
|
||||
notificationRules: Array<CreateChannelNotificationRule>;
|
||||
channelNameSiffix: string;
|
||||
}): Array<string> {
|
||||
const channelNames: Array<string> = [];
|
||||
|
||||
for (const notificationRule of data.notificationRules) {
|
||||
const workspaceRules: CreateChannelNotificationRule = notificationRule;
|
||||
|
||||
if (
|
||||
workspaceRules.shouldCreateNewChannel &&
|
||||
workspaceRules.newChannelTemplateName
|
||||
) {
|
||||
const newChannelName: string =
|
||||
workspaceRules.newChannelTemplateName ||
|
||||
`oneuptime-${data.notificationEventType.toLowerCase()}-`;
|
||||
|
||||
// add suffix and then check if it is already added or not.
|
||||
const channelName: string = newChannelName + data.channelNameSiffix;
|
||||
|
||||
if (!channelNames.includes(channelName)) {
|
||||
// if channel name is not already added then add it.
|
||||
channelNames.push(channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channelNames;
|
||||
}
|
||||
|
||||
private async getNotificationRules(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
notificationRuleEventType: NotificationRuleEventType;
|
||||
}): Promise<Array<Model>> {
|
||||
return await this.findBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
eventType: data.notificationRuleEventType,
|
||||
},
|
||||
select: {
|
||||
notificationRule: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
});
|
||||
}
|
||||
|
||||
private async getValuesBasedOnNotificationFor(data: {
|
||||
notificationFor: NotificationFor;
|
||||
}): Promise<{
|
||||
[key in NotificationRuleConditionCheckOn]:
|
||||
| string
|
||||
| Array<string>
|
||||
| undefined;
|
||||
}> {
|
||||
if (data.notificationFor.incidentId) {
|
||||
const incident: Incident | null = await IncidentService.findOneById({
|
||||
id: data.notificationFor.incidentId,
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
incidentSeverity: true,
|
||||
currentIncidentState: true,
|
||||
labels: true,
|
||||
monitors: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident) {
|
||||
throw new BadDataException("Incident ID not found");
|
||||
}
|
||||
|
||||
const monitorLabels: Array<Label> =
|
||||
await MonitorService.getLabelsForMonitors({
|
||||
monitorIds:
|
||||
incident.monitors?.map((monitor: Incident) => {
|
||||
return monitor.id!;
|
||||
}) || [],
|
||||
});
|
||||
|
||||
return {
|
||||
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentTitle]: incident.title || "",
|
||||
[NotificationRuleConditionCheckOn.IncidentDescription]:
|
||||
incident.description || "",
|
||||
[NotificationRuleConditionCheckOn.IncidentSeverity]:
|
||||
incident.incidentSeverity?._id?.toString() || "",
|
||||
[NotificationRuleConditionCheckOn.IncidentState]:
|
||||
incident.currentIncidentState?._id?.toString() || "",
|
||||
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentLabels]:
|
||||
incident.labels?.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorLabels]:
|
||||
monitorLabels.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.Monitors]:
|
||||
incident.monitors?.map((monitor: Incident) => {
|
||||
return monitor._id?.toString() || "";
|
||||
}) || [],
|
||||
};
|
||||
}
|
||||
|
||||
if (data.notificationFor.alertId) {
|
||||
const alert: Alert | null = await AlertService.findOneById({
|
||||
id: data.notificationFor.alertId,
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
alertSeverity: true,
|
||||
currentAlertState: true,
|
||||
labels: true,
|
||||
monitor: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alert) {
|
||||
throw new BadDataException("Alert ID not found");
|
||||
}
|
||||
|
||||
const monitorLabels: Array<Label> =
|
||||
await MonitorService.getLabelsForMonitors({
|
||||
monitorIds: alert?.monitor?.id ? [alert?.monitor?.id] : [],
|
||||
});
|
||||
|
||||
return {
|
||||
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertTitle]: alert.title || "",
|
||||
[NotificationRuleConditionCheckOn.AlertDescription]:
|
||||
alert.description || "",
|
||||
[NotificationRuleConditionCheckOn.AlertSeverity]:
|
||||
alert.alertSeverity?._id?.toString() || "",
|
||||
[NotificationRuleConditionCheckOn.AlertState]:
|
||||
alert.currentAlertState?._id?.toString() || "",
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertLabels]:
|
||||
alert.labels?.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.MonitorLabels]:
|
||||
monitorLabels.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.Monitors]: [
|
||||
alert.monitor?.id!.toString() || "",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (data.notificationFor.scheduledMaintenanceId) {
|
||||
const scheduledMaintenance: ScheduledMaintenance | null =
|
||||
await ScheduledMaintenanceService.findOneById({
|
||||
id: data.notificationFor.scheduledMaintenanceId,
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
currentScheduledMaintenanceState: true,
|
||||
labels: true,
|
||||
monitors: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!scheduledMaintenance) {
|
||||
throw new BadDataException("Scheduled Maintenance ID not found");
|
||||
}
|
||||
|
||||
const monitorLabels: Array<Label> =
|
||||
await MonitorService.getLabelsForMonitors({
|
||||
monitorIds:
|
||||
scheduledMaintenance.monitors?.map(
|
||||
(monitor: ScheduledMaintenance) => {
|
||||
return monitor.id!;
|
||||
},
|
||||
) || [],
|
||||
});
|
||||
|
||||
return {
|
||||
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]:
|
||||
scheduledMaintenance.title || "",
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
|
||||
scheduledMaintenance.description || "",
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]:
|
||||
scheduledMaintenance.currentScheduledMaintenanceState?._id?.toString() ||
|
||||
"",
|
||||
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorLabels]:
|
||||
monitorLabels.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
|
||||
scheduledMaintenance.labels?.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.Monitors]:
|
||||
scheduledMaintenance.monitors?.map(
|
||||
(monitor: ScheduledMaintenance) => {
|
||||
return monitor._id?.toString() || "";
|
||||
},
|
||||
) || [],
|
||||
};
|
||||
}
|
||||
|
||||
if (data.notificationFor.monitorStatusTimelineId) {
|
||||
const monitorStatusTimeline: MonitorStatusTimeline | null =
|
||||
await MonitorStatusTimelineService.findOneById({
|
||||
id: data.notificationFor.monitorStatusTimelineId,
|
||||
select: {
|
||||
monitor: {
|
||||
name: true,
|
||||
labels: true,
|
||||
monitorType: true,
|
||||
},
|
||||
monitorStatus: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!monitorStatusTimeline) {
|
||||
throw new BadDataException("Monitor Status Timeline ID not found");
|
||||
}
|
||||
|
||||
const monitorLabels: Array<Label> =
|
||||
monitorStatusTimeline.monitor?.labels || [];
|
||||
|
||||
return {
|
||||
[NotificationRuleConditionCheckOn.MonitorName]:
|
||||
monitorStatusTimeline.monitor?.name || "",
|
||||
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorType]:
|
||||
monitorStatusTimeline.monitor?.monitorType || undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorStatus]:
|
||||
monitorStatusTimeline.monitorStatus?._id?.toString() || "",
|
||||
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
|
||||
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
|
||||
[NotificationRuleConditionCheckOn.MonitorLabels]:
|
||||
monitorLabels.map((label: Label) => {
|
||||
return label._id?.toString() || "";
|
||||
}) || [],
|
||||
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
|
||||
undefined,
|
||||
[NotificationRuleConditionCheckOn.Monitors]: [
|
||||
monitorStatusTimeline.monitor?._id?.toString() || "",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadDataException("NotificationFor is not supported");
|
||||
}
|
||||
|
||||
public async getMatchingNotificationRules(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
notificationRuleEventType: NotificationRuleEventType;
|
||||
notificationFor: NotificationFor;
|
||||
}): Promise<Array<Model>> {
|
||||
const notificationRules: Array<Model> = await this.getNotificationRules({
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
notificationRuleEventType: data.notificationRuleEventType,
|
||||
});
|
||||
|
||||
const values: {
|
||||
[key in NotificationRuleConditionCheckOn]:
|
||||
| string
|
||||
| Array<string>
|
||||
| undefined;
|
||||
} = await this.getValuesBasedOnNotificationFor({
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
const matchingNotificationRules: Array<Model> = [];
|
||||
|
||||
for (const notificationRule of notificationRules) {
|
||||
if (
|
||||
WorkspaceNotificationRuleUtil.isRuleMatching({
|
||||
notificationRule:
|
||||
notificationRule.notificationRule as IncidentNotificationRule,
|
||||
values: values,
|
||||
})
|
||||
) {
|
||||
matchingNotificationRules.push(notificationRule);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingNotificationRules;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
112
Common/Server/Services/WorkspaceProjectAuthTokenService.ts
Normal file
112
Common/Server/Services/WorkspaceProjectAuthTokenService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackMiscData,
|
||||
} from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async getProjectAuth(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<Model | null> {
|
||||
return await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
authToken: true,
|
||||
workspaceProjectId: true,
|
||||
miscData: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getProjectAuths(data: {
|
||||
projectId: ObjectID;
|
||||
}): Promise<Array<Model>> {
|
||||
return await this.findBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
},
|
||||
select: {
|
||||
authToken: true,
|
||||
workspaceProjectId: true,
|
||||
miscData: true,
|
||||
workspaceType: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async doesExist(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<boolean> {
|
||||
return Boolean(await this.getProjectAuth(data));
|
||||
}
|
||||
|
||||
public async refreshAuthToken(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
authToken: string;
|
||||
workspaceProjectId: string;
|
||||
miscData: SlackMiscData;
|
||||
}): Promise<void> {
|
||||
let projectAuth: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectAuth) {
|
||||
projectAuth = new Model();
|
||||
|
||||
projectAuth.projectId = data.projectId;
|
||||
projectAuth.authToken = data.authToken;
|
||||
projectAuth.workspaceType = data.workspaceType;
|
||||
projectAuth.workspaceProjectId = data.workspaceProjectId;
|
||||
projectAuth.miscData = data.miscData;
|
||||
|
||||
await this.create({
|
||||
data: projectAuth,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.updateOneById({
|
||||
id: projectAuth.id!,
|
||||
data: {
|
||||
authToken: data.authToken,
|
||||
workspaceProjectId: data.workspaceProjectId,
|
||||
miscData: data.miscData,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
78
Common/Server/Services/WorkspaceSettingService.ts
Normal file
78
Common/Server/Services/WorkspaceSettingService.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackSettings,
|
||||
} from "Common/Models/DatabaseModels/WorkspaceSetting";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async doesExist(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<boolean> {
|
||||
return (
|
||||
(
|
||||
await this.countBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
skip: 0,
|
||||
limit: 1,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
})
|
||||
).toNumber() > 0
|
||||
);
|
||||
}
|
||||
|
||||
public async refreshSetting(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
settings: SlackSettings;
|
||||
}): Promise<void> {
|
||||
let workspaceSetting: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceSetting) {
|
||||
workspaceSetting = new Model();
|
||||
|
||||
workspaceSetting.projectId = data.projectId;
|
||||
workspaceSetting.settings = data.settings;
|
||||
workspaceSetting.workspaceType = data.workspaceType;
|
||||
|
||||
await this.create({
|
||||
data: workspaceSetting,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.updateOneById({
|
||||
id: workspaceSetting.id!,
|
||||
data: {
|
||||
settings: data.settings,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
89
Common/Server/Services/WorkspaceUserAuthTokenService.ts
Normal file
89
Common/Server/Services/WorkspaceUserAuthTokenService.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackMiscData,
|
||||
} from "Common/Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async doesExist(data: {
|
||||
projectId: ObjectID;
|
||||
userId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<boolean> {
|
||||
return (
|
||||
(
|
||||
await this.countBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
userId: data.userId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
skip: 0,
|
||||
limit: 1,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
})
|
||||
).toNumber() > 0
|
||||
);
|
||||
}
|
||||
|
||||
public async refreshAuthToken(data: {
|
||||
projectId: ObjectID;
|
||||
userId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
authToken: string;
|
||||
workspaceUserId: string;
|
||||
miscData: SlackMiscData;
|
||||
}): Promise<void> {
|
||||
let userAuth: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
userId: data.userId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userAuth) {
|
||||
userAuth = new Model();
|
||||
|
||||
userAuth.projectId = data.projectId;
|
||||
userAuth.userId = data.userId;
|
||||
userAuth.authToken = data.authToken;
|
||||
userAuth.workspaceType = data.workspaceType;
|
||||
userAuth.workspaceUserId = data.workspaceUserId;
|
||||
userAuth.miscData = data.miscData;
|
||||
|
||||
await this.create({
|
||||
data: userAuth,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.updateOneById({
|
||||
id: userAuth.id!,
|
||||
data: {
|
||||
authToken: data.authToken,
|
||||
workspaceUserId: data.workspaceUserId,
|
||||
miscData: data.miscData,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
@@ -8,7 +8,7 @@ import { JSONObject } from "Common/Types/JSON";
|
||||
import ComponentMetadata, { Port } from "Common/Types/Workflow/Component";
|
||||
import ComponentID from "Common/Types/Workflow/ComponentID";
|
||||
import SlackComponents from "Common/Types/Workflow/Components/Slack";
|
||||
import SlackUtil from "../../../../Utils/Slack";
|
||||
import SlackUtil from "../../../../Utils/Workspace/Slack/Slack";
|
||||
|
||||
export default class SendMessageToChannel extends ComponentCode {
|
||||
public constructor() {
|
||||
@@ -69,7 +69,7 @@ export default class SendMessageToChannel extends ComponentCode {
|
||||
|
||||
try {
|
||||
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
|
||||
apiResult = await SlackUtil.sendMessageToChannel({
|
||||
apiResult = await SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: args["webhook-url"] as URL,
|
||||
text: args["text"] as string,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,8 @@ export interface OneUptimeRequest extends express.Request {
|
||||
userAuthorization?: JSONWebTokenData;
|
||||
tenantId?: ObjectID;
|
||||
userGlobalAccessPermission?: UserGlobalAccessPermission;
|
||||
userTenantAccessPermission?: Dictionary<UserTenantAccessPermission>; // tenantId <-> UserTenantAccessPermission
|
||||
userTenantAccessPermission?: Dictionary<UserTenantAccessPermission>; // tenantId <-> UserTenantAccessPermission;
|
||||
rawBody?: string; // raw body of the request before json parsing.
|
||||
}
|
||||
|
||||
export interface OneUptimeResponse extends express.Response {
|
||||
|
||||
@@ -16,11 +16,7 @@ import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import BasicInfrastructureMetrics from "Common/Types/Infrastructure/BasicMetrics";
|
||||
import ReturnResult from "Common/Types/IsolatedVM/ReturnResult";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import {
|
||||
CheckOn,
|
||||
CriteriaFilter,
|
||||
FilterCondition,
|
||||
} from "Common/Types/Monitor/CriteriaFilter";
|
||||
import { CheckOn, CriteriaFilter } from "Common/Types/Monitor/CriteriaFilter";
|
||||
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
||||
import MonitorCriteria from "Common/Types/Monitor/MonitorCriteria";
|
||||
import MonitorCriteriaInstance from "Common/Types/Monitor/MonitorCriteriaInstance";
|
||||
@@ -57,6 +53,7 @@ import MonitorMetricType from "../../../Types/Monitor/MonitorMetricType";
|
||||
import TelemetryUtil from "../Telemetry/Telemetry";
|
||||
import MetricMonitorCriteria from "./Criteria/MetricMonitorCriteria";
|
||||
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
||||
import FilterCondition from "../../../Types/Filter/FilterCondition";
|
||||
|
||||
export default class MonitorResourceUtil {
|
||||
public static async monitorResource(
|
||||
|
||||
@@ -22,6 +22,7 @@ import Exception from "Common/Types/Exception/Exception";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import ListData from "Common/Types/ListData";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import Route from "../../Types/API/Route";
|
||||
|
||||
export default class Response {
|
||||
public static sendEmptySuccessResponse(
|
||||
@@ -33,6 +34,14 @@ export default class Response {
|
||||
oneUptimeResponse.status(200).send({} as EmptyResponse);
|
||||
}
|
||||
|
||||
public static sendFileByPath(
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
path: string,
|
||||
): void {
|
||||
res.sendFile(path);
|
||||
}
|
||||
|
||||
public static sendCustomResponse(
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
@@ -162,9 +171,9 @@ export default class Response {
|
||||
public static redirect(
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
url: URL,
|
||||
to: URL | Route,
|
||||
): void {
|
||||
return res.redirect(url.toString());
|
||||
return res.redirect(to.toString());
|
||||
}
|
||||
|
||||
public static sendJsonArrayResponse(
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import API from "Common/Utils/API";
|
||||
|
||||
export default class SlackUtil {
|
||||
public static async sendMessageToChannel(data: {
|
||||
url: URL;
|
||||
text: string;
|
||||
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
|
||||
let apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null = null;
|
||||
|
||||
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
|
||||
apiResult = await API.post(data.url, {
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${data.text}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import Express, {
|
||||
ExpressStatic,
|
||||
ExpressUrlEncoded,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
RequestHandler,
|
||||
} from "./Express";
|
||||
import logger from "./Logger";
|
||||
@@ -82,7 +83,7 @@ app.set("view engine", "ejs");
|
||||
* https://stackoverflow.com/questions/19917401/error-request-entity-too-large
|
||||
*/
|
||||
|
||||
app.use((req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
app.use((req: OneUptimeRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
if (req.headers["content-encoding"] === "gzip") {
|
||||
const buffers: any = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import WorkspaceBase from "../WorkspaceBase";
|
||||
|
||||
export default class MicrosoftTeams extends WorkspaceBase {}
|
||||
325
Common/Server/Utils/Workspace/Slack/Slack.ts
Normal file
325
Common/Server/Utils/Workspace/Slack/Slack.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import API from "Common/Utils/API";
|
||||
import WorkspaceMessagePayload, {
|
||||
WorkspaceMessagePayloadButton,
|
||||
WorkspacePayloadHeader,
|
||||
WorkspacePayloadMarkdown,
|
||||
} from "../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import logger from "../../Logger";
|
||||
import Dictionary from "../../../../Types/Dictionary";
|
||||
import BadRequestException from "../../../../Types/Exception/BadRequestException";
|
||||
import WorkspaceBase, { WorkspaceChannel } from "../WorkspaceBase";
|
||||
import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
|
||||
|
||||
export default class SlackUtil extends WorkspaceBase {
|
||||
public static override async inviteUserToChannel(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
workspaceUserId: string;
|
||||
}): Promise<void> {
|
||||
const channelId: string = (
|
||||
await this.getWorkspaceChannelFromChannelId({
|
||||
authToken: data.authToken,
|
||||
channelId: data.channelName,
|
||||
})
|
||||
).id;
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/conversations.invite"),
|
||||
{
|
||||
channel: channelId,
|
||||
users: data.workspaceUserId,
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
|
||||
throw new BadRequestException("Invalid response");
|
||||
}
|
||||
}
|
||||
|
||||
public static override async createChannelsIfDoesNotExist(data: {
|
||||
authToken: string;
|
||||
channelNames: Array<string>;
|
||||
}): Promise<Array<WorkspaceChannel>> {
|
||||
// check existing channels and only create if they dont exist.
|
||||
const workspaceChannels: Array<WorkspaceChannel> = [];
|
||||
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
|
||||
for (const channelName of data.channelNames) {
|
||||
if (existingWorkspaceChannels[channelName]) {
|
||||
logger.debug(`Channel ${channelName} already exists.`);
|
||||
|
||||
workspaceChannels.push(existingWorkspaceChannels[channelName]!);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const channel: WorkspaceChannel = await this.createChannel({
|
||||
authToken: data.authToken,
|
||||
channelName: channelName,
|
||||
});
|
||||
|
||||
if (channel) {
|
||||
workspaceChannels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceChannels;
|
||||
}
|
||||
|
||||
public static override async getWorkspaceChannelFromChannelId(data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get<JSONObject>(
|
||||
URL.fromString("https://slack.com/api/conversations.info"),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
params: {
|
||||
channel: data.channelId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (
|
||||
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["name"]
|
||||
) {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
|
||||
return {
|
||||
name: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
|
||||
"name"
|
||||
] as string,
|
||||
id: data.channelId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
};
|
||||
}
|
||||
|
||||
public static override async getAllWorkspaceChannels(data: {
|
||||
authToken: string;
|
||||
}): Promise<Dictionary<WorkspaceChannel>> {
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get<JSONObject>(
|
||||
URL.fromString("https://slack.com/api/conversations.list"),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const channels: Dictionary<WorkspaceChannel> = {};
|
||||
|
||||
for (const channel of (response.jsonData as JSONObject)[
|
||||
"channels"
|
||||
] as Array<JSONObject>) {
|
||||
if (!channel["id"] || !channel["name"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
channels[channel["name"].toString()] = {
|
||||
id: channel["id"] as string,
|
||||
name: channel["name"] as string,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
};
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public static override async sendMessage(data: {
|
||||
workspaceMessagePayload: WorkspaceMessagePayload;
|
||||
authToken: string; // which auth token should we use to send.
|
||||
}): Promise<void> {
|
||||
logger.debug("Notify Slack");
|
||||
logger.debug(data);
|
||||
|
||||
const blocks: Array<JSONObject> = this.getBlocksFromWorkspaceMessagePayload(
|
||||
data.workspaceMessagePayload,
|
||||
);
|
||||
|
||||
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
|
||||
const channelIdsToPostTo: Array<string> = [];
|
||||
|
||||
for (const channelName of data.workspaceMessagePayload.channelNames) {
|
||||
// get channel ids from existingWorkspaceChannels. IF channel doesn't exist, create it if createChannelsIfItDoesNotExist is true.
|
||||
let channel: WorkspaceChannel | null = null;
|
||||
|
||||
if (existingWorkspaceChannels[channelName]) {
|
||||
channel = existingWorkspaceChannels[channelName]!;
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
channelIdsToPostTo.push(channel.id);
|
||||
} else {
|
||||
logger.debug(`Channel ${channelName} does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const channelId of channelIdsToPostTo) {
|
||||
try {
|
||||
// try catch here to prevent failure of one channel to prevent posting to other channels.
|
||||
await this.sendPayloadBlocksToChannel({
|
||||
authToken: data.authToken,
|
||||
channelId: channelId,
|
||||
blocks: blocks,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static override async sendPayloadBlocksToChannel(data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
blocks: Array<JSONObject>;
|
||||
}): Promise<void> {
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/chat.postMessage"),
|
||||
{
|
||||
channel: data.channelId,
|
||||
blocks: data.blocks,
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
|
||||
throw new BadRequestException("Invalid response");
|
||||
}
|
||||
}
|
||||
|
||||
public static override async createChannel(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post(
|
||||
URL.fromString("https://slack.com/api/conversations.create"),
|
||||
{
|
||||
name: data.channelName,
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (
|
||||
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["id"] ||
|
||||
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["name"]
|
||||
) {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
|
||||
return {
|
||||
id: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
|
||||
"id"
|
||||
] as string,
|
||||
name: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
|
||||
"name"
|
||||
] as string,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
};
|
||||
}
|
||||
|
||||
public static override getHeaderBlock(data: {
|
||||
payloadHeaderBlock: WorkspacePayloadHeader;
|
||||
}): JSONObject {
|
||||
return {
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: data.payloadHeaderBlock.text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getMarkdownBlock(data: {
|
||||
payloadMarkdownBlock: WorkspacePayloadMarkdown;
|
||||
}): JSONObject {
|
||||
return {
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: data.payloadMarkdownBlock.text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getButtonBlock(data: {
|
||||
payloadButtonBlock: WorkspaceMessagePayloadButton;
|
||||
}): JSONObject {
|
||||
return {
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: data.payloadButtonBlock.title,
|
||||
},
|
||||
value: data.payloadButtonBlock.title,
|
||||
action_id: data.payloadButtonBlock.title,
|
||||
};
|
||||
}
|
||||
|
||||
public static override async sendMessageToChannelViaIncomingWebhook(data: {
|
||||
url: URL;
|
||||
text: string;
|
||||
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
|
||||
let apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null = null;
|
||||
|
||||
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
|
||||
apiResult = await API.post(data.url, {
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${data.text}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
}
|
||||
198
Common/Server/Utils/Workspace/Slack/app-manifest-temp.json
Normal file
198
Common/Server/Utils/Workspace/Slack/app-manifest-temp.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"display_information": {
|
||||
"name": "OneUptime",
|
||||
"description": "The Complete Open-Source Observability Platform",
|
||||
"background_color": "#000000",
|
||||
"long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
|
||||
},
|
||||
"features": {
|
||||
"app_home": {
|
||||
"home_tab_enabled": true,
|
||||
"messages_tab_enabled": false,
|
||||
"messages_tab_read_only_enabled": false
|
||||
},
|
||||
"bot_user": {
|
||||
"display_name": "OneUptime",
|
||||
"always_online": true
|
||||
},
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Create Scheduled Event",
|
||||
"type": "global",
|
||||
"callback_id": "create-scheduled-maintenance",
|
||||
"description": "Create a new scheduled event in OneUptime"
|
||||
},
|
||||
{
|
||||
"name": "Create New Incident",
|
||||
"type": "global",
|
||||
"callback_id": "create-incident",
|
||||
"description": "Creates a new incident in OneUptime"
|
||||
}
|
||||
],
|
||||
"slash_commands": [
|
||||
{
|
||||
"command": "/oneuptime",
|
||||
"url": "https://local.genosyn.com/api/slack/command",
|
||||
"description": "OneUptime command",
|
||||
"usage_hint": "incident, scheduled event.",
|
||||
"should_escape": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"oauth_config": {
|
||||
"redirect_urls": [
|
||||
"https://local.genosyn.com/api/slack/auth"
|
||||
],
|
||||
"scopes": {
|
||||
"user": [
|
||||
"users:read",
|
||||
"users:read.email",
|
||||
"bookmarks:read",
|
||||
"workflows.templates:write",
|
||||
"workflows.templates:read",
|
||||
"users:write",
|
||||
"users.profile:write",
|
||||
"users.profile:read",
|
||||
"usergroups:write",
|
||||
"usergroups:read",
|
||||
"team:read",
|
||||
"team.preferences:read",
|
||||
"team.billing:read",
|
||||
"stars:write",
|
||||
"stars:read",
|
||||
"search:read",
|
||||
"remote_files:share",
|
||||
"reminders:read",
|
||||
"remote_files:read",
|
||||
"reminders:write",
|
||||
"reactions:write",
|
||||
"reactions:read",
|
||||
"profile",
|
||||
"pins:write",
|
||||
"pins:read",
|
||||
"openid",
|
||||
"mpim:write.topic",
|
||||
"mpim:write",
|
||||
"mpim:read",
|
||||
"mpim:history",
|
||||
"links:write",
|
||||
"links:read",
|
||||
"links.embed:write",
|
||||
"im:write.topic",
|
||||
"im:write",
|
||||
"im:read",
|
||||
"im:history",
|
||||
"identity.team",
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"identity.avatar",
|
||||
"identify",
|
||||
"groups:write.topic",
|
||||
"groups:write.invites",
|
||||
"groups:write",
|
||||
"groups:read",
|
||||
"groups:history",
|
||||
"files:write",
|
||||
"files:read",
|
||||
"emoji:read",
|
||||
"email",
|
||||
"dnd:write",
|
||||
"dnd:read",
|
||||
"chat:write",
|
||||
"channels:write.topic",
|
||||
"channels:write.invites",
|
||||
"channels:write",
|
||||
"channels:read",
|
||||
"channels:history",
|
||||
"canvases:write",
|
||||
"canvases:read",
|
||||
"calls:write",
|
||||
"calls:read",
|
||||
"bookmarks:write"
|
||||
],
|
||||
"bot": [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"bookmarks:read",
|
||||
"bookmarks:write",
|
||||
"calls:write",
|
||||
"calls:read",
|
||||
"canvases:read",
|
||||
"canvases:write",
|
||||
"channels:history",
|
||||
"channels:join",
|
||||
"channels:manage",
|
||||
"channels:read",
|
||||
"channels:write.invites",
|
||||
"channels:write.topic",
|
||||
"chat:write",
|
||||
"chat:write.customize",
|
||||
"chat:write.public",
|
||||
"commands",
|
||||
"conversations.connect:manage",
|
||||
"conversations.connect:read",
|
||||
"files:read",
|
||||
"conversations.connect:write",
|
||||
"dnd:read",
|
||||
"emoji:read",
|
||||
"files:write",
|
||||
"groups:history",
|
||||
"groups:read",
|
||||
"groups:write",
|
||||
"groups:write.invites",
|
||||
"groups:write.topic",
|
||||
"im:read",
|
||||
"im:history",
|
||||
"incoming-webhook",
|
||||
"links.embed:write",
|
||||
"im:write.topic",
|
||||
"im:write",
|
||||
"links:read",
|
||||
"links:write",
|
||||
"metadata.message:read",
|
||||
"mpim:history",
|
||||
"pins:read",
|
||||
"users:read.email",
|
||||
"users:read",
|
||||
"mpim:read",
|
||||
"mpim:write",
|
||||
"mpim:write.topic",
|
||||
"pins:write",
|
||||
"reactions:write",
|
||||
"reactions:read",
|
||||
"reminders:read",
|
||||
"search:read.files",
|
||||
"remote_files:write",
|
||||
"remote_files:share",
|
||||
"reminders:write",
|
||||
"remote_files:read",
|
||||
"search:read.im",
|
||||
"team.billing:read",
|
||||
"team.preferences:read",
|
||||
"search:read.mpim",
|
||||
"search:read.private",
|
||||
"search:read.public",
|
||||
"usergroups:read",
|
||||
"usergroups:write",
|
||||
"triggers:write",
|
||||
"team:read",
|
||||
"triggers:read",
|
||||
"users.profile:read",
|
||||
"users:write",
|
||||
"workflows.templates:read",
|
||||
"workflow.steps:execute",
|
||||
"workflows.templates:write"
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"interactivity": {
|
||||
"is_enabled": true,
|
||||
"request_url": "https://local.genosyn.com/api/slack/interactive",
|
||||
"message_menu_options_url": "https://local.genosyn.com/api/slack/options-load"
|
||||
},
|
||||
"org_deploy_enabled": true,
|
||||
"socket_mode_enabled": false,
|
||||
"token_rotation_enabled": false
|
||||
}
|
||||
}
|
||||
67
Common/Server/Utils/Workspace/Slack/app-manifest.json
Normal file
67
Common/Server/Utils/Workspace/Slack/app-manifest.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"display_information": {
|
||||
"name": "OneUptime",
|
||||
"description": "The Complete Open-Source Observability Platform",
|
||||
"background_color": "#000000",
|
||||
"long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
|
||||
},
|
||||
"features": {
|
||||
"app_home": {
|
||||
"home_tab_enabled": true,
|
||||
"messages_tab_enabled": false,
|
||||
"messages_tab_read_only_enabled": false
|
||||
},
|
||||
"bot_user": {
|
||||
"display_name": "OneUptime",
|
||||
"always_online": true
|
||||
},
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Create Scheduled Event",
|
||||
"type": "global",
|
||||
"callback_id": "create-scheduled-maintenance",
|
||||
"description": "Create a new scheduled event in OneUptime"
|
||||
},
|
||||
{
|
||||
"name": "Create New Incident",
|
||||
"type": "global",
|
||||
"callback_id": "create-incident",
|
||||
"description": "Creates a new incident in OneUptime"
|
||||
}
|
||||
],
|
||||
"slash_commands": [
|
||||
{
|
||||
"command": "/oneuptime",
|
||||
"url": "https://local.genosyn.com/api/slack/command",
|
||||
"description": "OneUptime command",
|
||||
"usage_hint": "incident, scheduled event.",
|
||||
"should_escape": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"oauth_config": {
|
||||
"redirect_urls": [
|
||||
"https://local.genosyn.com/api/slack/auth"
|
||||
],
|
||||
"scopes": {
|
||||
"user": [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"email"
|
||||
],
|
||||
"bot": [
|
||||
"commands"
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"interactivity": {
|
||||
"is_enabled": true,
|
||||
"request_url": "https://local.genosyn.com/api/slack/interactive",
|
||||
"message_menu_options_url": "https://local.genosyn.com/api/slack/options-load"
|
||||
},
|
||||
"org_deploy_enabled": true,
|
||||
"socket_mode_enabled": false,
|
||||
"token_rotation_enabled": false
|
||||
}
|
||||
}
|
||||
23
Common/Server/Utils/Workspace/Workspace.ts
Normal file
23
Common/Server/Utils/Workspace/Workspace.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
|
||||
import WorkspaceBase from "./WorkspaceBase";
|
||||
import SlackWorkspace from "./Slack/Slack";
|
||||
import MicrosoftTeamsWorkspace from "./MicrosoftTeams/MicrosoftTeams";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
|
||||
export default class WorkspaceUtil {
|
||||
public static getWorkspaceTypeUtil(
|
||||
workspaceType: WorkspaceType,
|
||||
): typeof WorkspaceBase {
|
||||
if (workspaceType === WorkspaceType.Slack) {
|
||||
return SlackWorkspace;
|
||||
}
|
||||
|
||||
if (workspaceType === WorkspaceType.MicrosoftTeams) {
|
||||
return MicrosoftTeamsWorkspace;
|
||||
}
|
||||
|
||||
throw new BadDataException(
|
||||
`Workspace type ${workspaceType} is not supported`,
|
||||
);
|
||||
}
|
||||
}
|
||||
169
Common/Server/Utils/Workspace/WorkspaceBase.ts
Normal file
169
Common/Server/Utils/Workspace/WorkspaceBase.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import NotImplementedException from "../../../Types/Exception/NotImplementedException";
|
||||
import { JSONObject } from "../../../Types/JSON";
|
||||
import WorkspaceChannelInvitationPayload from "../../../Types/Workspace/WorkspaceChannelInvitationPayload";
|
||||
import WorkspaceMessagePayload, {
|
||||
WorkspaceMessagePayloadButton,
|
||||
WorkspacePayloadButtons,
|
||||
WorkspacePayloadHeader,
|
||||
WorkspacePayloadMarkdown,
|
||||
} from "../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
|
||||
import logger from "../Logger";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
export interface WorkspaceChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
workspaceType: WorkspaceType;
|
||||
}
|
||||
|
||||
export default class WorkspaceBase {
|
||||
public static async sendPayloadBlocksToChannel(_data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
blocks: Array<JSONObject>;
|
||||
}): Promise<void> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async inviteUsersToChannels(data: {
|
||||
authToken: string;
|
||||
workspaceChannelInvitationPayload: WorkspaceChannelInvitationPayload;
|
||||
}): Promise<void> {
|
||||
for (const channelName of data.workspaceChannelInvitationPayload
|
||||
.channelNames) {
|
||||
await this.inviteUsersToChannel({
|
||||
authToken: data.authToken,
|
||||
channelName: channelName,
|
||||
workspaceUserIds:
|
||||
data.workspaceChannelInvitationPayload.workspaceUserIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static async inviteUsersToChannel(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
workspaceUserIds: Array<string>;
|
||||
}): Promise<void> {
|
||||
for (const userId of data.workspaceUserIds) {
|
||||
await this.inviteUserToChannel({
|
||||
authToken: data.authToken,
|
||||
channelName: data.channelName,
|
||||
workspaceUserId: userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static async inviteUserToChannel(_data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
workspaceUserId: string;
|
||||
}): Promise<void> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async createChannelsIfDoesNotExist(_data: {
|
||||
authToken: string;
|
||||
channelNames: Array<string>;
|
||||
}): Promise<Array<WorkspaceChannel>> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async getWorkspaceChannelFromChannelId(_data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async sendMessage(_data: {
|
||||
workspaceMessagePayload: WorkspaceMessagePayload;
|
||||
authToken: string; // which auth token should we use to send.
|
||||
}): Promise<void> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async getAllWorkspaceChannels(_data: {
|
||||
authToken: string;
|
||||
}): Promise<Dictionary<WorkspaceChannel>> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async createChannel(_data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static getHeaderBlock(_data: {
|
||||
payloadHeaderBlock: WorkspacePayloadHeader;
|
||||
}): JSONObject {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static getMarkdownBlock(_data: {
|
||||
payloadMarkdownBlock: WorkspacePayloadMarkdown;
|
||||
}): JSONObject {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static getButtonBlock(_data: {
|
||||
payloadButtonBlock: WorkspaceMessagePayloadButton;
|
||||
}): JSONObject {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static getBlocksFromWorkspaceMessagePayload(
|
||||
data: WorkspaceMessagePayload,
|
||||
): Array<JSONObject> {
|
||||
const blocks: Array<JSONObject> = [];
|
||||
const buttons: Array<JSONObject> = [];
|
||||
for (const block of data.messageBlocks) {
|
||||
switch (block._type) {
|
||||
case "WorkspacePayloadHeader":
|
||||
blocks.push(
|
||||
this.getHeaderBlock({
|
||||
payloadHeaderBlock: block as WorkspacePayloadHeader,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "WorkspacePayloadMarkdown":
|
||||
blocks.push(
|
||||
this.getMarkdownBlock({
|
||||
payloadMarkdownBlock: block as WorkspacePayloadMarkdown,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "WorkspacePayloadButtons":
|
||||
for (const button of (block as WorkspacePayloadButtons).buttons) {
|
||||
buttons.push(
|
||||
this.getButtonBlock({
|
||||
payloadButtonBlock: button,
|
||||
}),
|
||||
);
|
||||
}
|
||||
blocks.push({
|
||||
type: "actions",
|
||||
elements: buttons,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
logger.error("Unknown block type: " + block._type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public static async sendMessageToChannelViaIncomingWebhook(_data: {
|
||||
url: URL;
|
||||
text: string;
|
||||
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Dictionary } from "lodash";
|
||||
import DatabaseProperty from "../Database/DatabaseProperty";
|
||||
import BadDataException from "../Exception/BadDataException";
|
||||
import { JSONObject, ObjectType } from "../JSON";
|
||||
@@ -98,4 +99,21 @@ export default class Route extends DatabaseProperty {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public addQueryParams(queryParams: Dictionary<string>): Route {
|
||||
// make sure route ends with "?" if it doesn't have any query params
|
||||
|
||||
if (!this.route.includes("?")) {
|
||||
this.route += "?";
|
||||
}
|
||||
|
||||
for (const key in queryParams) {
|
||||
this.route += `${key}=${queryParams[key]}&`;
|
||||
}
|
||||
|
||||
//remove last "&" from route
|
||||
this.route = this.route.substring(0, this.route.length - 1);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class Domain extends DatabaseProperty {
|
||||
"|",
|
||||
);
|
||||
const secondTLDs: Array<string> =
|
||||
"ac|academy|accountant|accountants|actor|adult|aero|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|sbs|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc|za|ovh|lol|africa".split(
|
||||
"ac|academy|accountant|accountants|actor|adult|aero|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|sbs|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc|za|ovh|lol|africa|top".split(
|
||||
"|",
|
||||
);
|
||||
|
||||
|
||||
@@ -186,6 +186,10 @@ export default class Recurring extends DatabaseProperty {
|
||||
return arrayToReturn;
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return `${this.intervalCount} ${this.intervalType}`;
|
||||
}
|
||||
|
||||
protected static override toDatabase(
|
||||
value: Recurring | Array<Recurring> | FindOperator<Recurring>,
|
||||
): JSONObject | Array<JSONObject> | null {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
enum FilterCondition {
|
||||
And = "And",
|
||||
Or = "Or",
|
||||
All = "All",
|
||||
Any = "Any",
|
||||
}
|
||||
|
||||
export default FilterCondition;
|
||||
|
||||
@@ -110,12 +110,38 @@ export enum FilterType {
|
||||
IsNotExecuting = "Is Not Executing",
|
||||
}
|
||||
|
||||
export enum FilterCondition {
|
||||
All = "All",
|
||||
Any = "Any",
|
||||
}
|
||||
|
||||
export class CriteriaFilterUtil {
|
||||
public static hasValueField(data: {
|
||||
checkOn: CheckOn;
|
||||
filterType: FilterType | undefined;
|
||||
}): boolean {
|
||||
const { checkOn } = data;
|
||||
|
||||
if (checkOn === CheckOn.IsOnline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
checkOn === CheckOn.IsValidCertificate ||
|
||||
checkOn === CheckOn.IsSelfSignedCertificate ||
|
||||
checkOn === CheckOn.IsExpiredCertificate ||
|
||||
checkOn === CheckOn.IsNotAValidCertificate
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
FilterType.IsEmpty === data.filterType ||
|
||||
FilterType.IsNotEmpty === data.filterType ||
|
||||
FilterType.True === data.filterType ||
|
||||
FilterType.False === data.filterType
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static getEvaluateOverTimeTypeByCriteriaFilter(
|
||||
criteriaFilter: CriteriaFilter | undefined,
|
||||
): Array<EvaluateOverTimeType> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import DatabaseProperty from "../Database/DatabaseProperty";
|
||||
import BadDataException from "../Exception/BadDataException";
|
||||
import FilterCondition from "../Filter/FilterCondition";
|
||||
import { JSONObject, ObjectType } from "../JSON";
|
||||
import JSONFunctions from "../JSONFunctions";
|
||||
import ObjectID from "../ObjectID";
|
||||
@@ -8,9 +9,9 @@ import { CriteriaAlert } from "./CriteriaAlert";
|
||||
import {
|
||||
CheckOn,
|
||||
CriteriaFilter,
|
||||
FilterCondition,
|
||||
FilterType,
|
||||
EvaluateOverTimeType,
|
||||
CriteriaFilterUtil,
|
||||
} from "./CriteriaFilter";
|
||||
import { CriteriaIncident } from "./CriteriaIncident";
|
||||
import MonitorType from "./MonitorType";
|
||||
@@ -699,19 +700,19 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
monitorType: MonitorType,
|
||||
): string | null {
|
||||
if (!value.data) {
|
||||
return "Monitor Step is required";
|
||||
return `Monitor Step is required.`;
|
||||
}
|
||||
|
||||
if (value.data.filters.length === 0) {
|
||||
return "Monitor Criteria filter is required";
|
||||
return `Filter is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!value.data.name) {
|
||||
return "Monitor Criteria name is required";
|
||||
return `Name is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!value.data.description) {
|
||||
return "Monitor Criteria description is required";
|
||||
return `Description is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
for (const incident of value.data.incidents) {
|
||||
@@ -720,21 +721,39 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
}
|
||||
|
||||
if (!incident.title) {
|
||||
return "Monitor Criteria incident title is required";
|
||||
return `Incident title is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!incident.description) {
|
||||
return "Monitor Criteria incident description is required";
|
||||
return `Incident description is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!incident.incidentSeverityId) {
|
||||
return "Monitor Criteria incident severity is required";
|
||||
return `Incident severity is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const alert of value.data.alerts) {
|
||||
if (!alert) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!alert.title) {
|
||||
return `Alert title is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!alert.description) {
|
||||
return `Alert description is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (!alert.alertSeverityId) {
|
||||
return `Alert severity is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const filter of value.data.filters) {
|
||||
if (!filter.checkOn) {
|
||||
return "Monitor Criteria filter check on is required";
|
||||
return `Filter Type is required for criteria "${value.data.name}"`;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -742,7 +761,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
filter.checkOn !== CheckOn.IsOnline &&
|
||||
filter.checkOn !== CheckOn.ResponseTime
|
||||
) {
|
||||
return "Ping Monitor cannot have filter criteria: " + filter.checkOn;
|
||||
return "Ping Monitor cannot have filter type: " + filter.checkOn;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -751,6 +770,17 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
) {
|
||||
return "Disk Path is required for Disk Usage Percent";
|
||||
}
|
||||
|
||||
if (
|
||||
CriteriaFilterUtil.hasValueField({
|
||||
checkOn: filter.checkOn,
|
||||
filterType: filter.filterType,
|
||||
})
|
||||
) {
|
||||
if (!filter.value && filter.value !== 0) {
|
||||
return `Value is required for criteria "${value.data.name}" on filter type: ${filter.checkOn}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -252,7 +252,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
}
|
||||
|
||||
if (
|
||||
!MonitorCriteria.getValidationError(
|
||||
MonitorCriteria.getValidationError(
|
||||
value.data.monitorCriteria,
|
||||
monitorType,
|
||||
)
|
||||
|
||||
@@ -607,6 +607,11 @@ enum Permission {
|
||||
DeleteTableView = "DeleteTableView",
|
||||
EditTableView = "EditTableView",
|
||||
ReadTableView = "ReadTableView",
|
||||
|
||||
CreateWorkspaceNotificationRule = "CreateWorkspaceNotificationRule",
|
||||
DeleteWorkspaceNotificationRule = "DeleteWorkspaceNotificationRule",
|
||||
EditWorkspaceNotificationRule = "EditWorkspaceNotificationRule",
|
||||
ReadWorkspaceNotificationRule = "ReadWorkspaceNotificationRule",
|
||||
}
|
||||
|
||||
export class PermissionHelper {
|
||||
@@ -1039,6 +1044,35 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateWorkspaceNotificationRule,
|
||||
title: "Create Workspace Notification Rule",
|
||||
description: "This permission can create alert states this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.DeleteWorkspaceNotificationRule,
|
||||
title: "Delete Workspace Notification Rule",
|
||||
description: "This permission can delete alert states of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.EditWorkspaceNotificationRule,
|
||||
title: "Edit Workspace Notification Rule",
|
||||
description: "This permission can edit alert states of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.ReadWorkspaceNotificationRule,
|
||||
title: "Read Workspace Notification Rule",
|
||||
description: "This permission can read alert states of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateIncidentStateTimeline,
|
||||
title: "Create Incident State Timeline",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import FilterCondition from "../../Filter/FilterCondition";
|
||||
import NotificationRuleCondition from "./NotificationRuleCondition";
|
||||
|
||||
export default interface BaseNotificationRule {
|
||||
_type: string;
|
||||
// filters for notification rule
|
||||
filterCondition: FilterCondition; // and OR or. Default is AND
|
||||
filters: Array<NotificationRuleCondition>; // if this array is empty then it will be considered as all filters are matched.
|
||||
|
||||
shouldPostToExistingChannel: boolean;
|
||||
existingChannelNames: string; // seperate by comma
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import ObjectID from "../../ObjectID";
|
||||
import BaseNotificationRule from "./BaseNotificationRule";
|
||||
|
||||
export default interface CreateChannelNotificationRule
|
||||
extends BaseNotificationRule {
|
||||
_type: string;
|
||||
|
||||
// if filters match then do:
|
||||
shouldCreateNewChannel: boolean;
|
||||
inviteTeamsToNewChannel: Array<ObjectID>;
|
||||
inviteUsersToNewChannel: Array<ObjectID>;
|
||||
shouldInviteOwnersToNewChannel: boolean;
|
||||
newChannelTemplateName: string;
|
||||
}
|
||||
8
Common/Types/Workspace/NotificationRules/EventType.ts
Normal file
8
Common/Types/Workspace/NotificationRules/EventType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
enum NotificationRuleEventType {
|
||||
Incident = "Incident",
|
||||
MonitorStatus = "Monitor Status",
|
||||
Alert = "Alert",
|
||||
ScheduledMaintenance = "Scheduled Maintenance",
|
||||
}
|
||||
|
||||
export default NotificationRuleEventType;
|
||||
@@ -0,0 +1,358 @@
|
||||
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
||||
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
||||
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
||||
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
||||
import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
|
||||
import ScheduledMaintenanceState from "../../../Models/DatabaseModels/ScheduledMaintenanceState";
|
||||
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
|
||||
import WorkspaceType from "../WorkspaceType";
|
||||
import NotificationRuleEventType from "./EventType";
|
||||
import IncidentNotificationRule from "./NotificationRuleTypes/IncidentNotificationRule";
|
||||
|
||||
export enum NotificationRuleConditionCheckOn {
|
||||
MonitorName = "Monitor Name",
|
||||
IncidentTitle = "Incident Title",
|
||||
IncidentDescription = "Incident Description",
|
||||
IncidentSeverity = "Incident Severity",
|
||||
IncidentState = "Incident State",
|
||||
MonitorType = "Monitor Type",
|
||||
MonitorStatus = "Monitor Status",
|
||||
AlertTitle = "Alert Title",
|
||||
AlertDescription = "Alert Description",
|
||||
AlertSeverity = "Alert Severity",
|
||||
AlertState = "Alert State",
|
||||
ScheduledMaintenanceTitle = "Scheduled Maintenance Title",
|
||||
ScheduledMaintenanceDescription = "Scheduled Maintenance Description",
|
||||
ScheduledMaintenanceState = "Scheduled Maintenance State",
|
||||
IncidentLabels = "Incident Labels",
|
||||
AlertLabels = "Alert Labels",
|
||||
MonitorLabels = "Monitor Labels",
|
||||
ScheduledMaintenanceLabels = "Scheduled Maintenance Labels",
|
||||
Monitors = "Monitors", // like monitor contains in the incident or scheduled event.
|
||||
}
|
||||
|
||||
export enum ConditionType {
|
||||
EqualTo = "Equal To",
|
||||
NotEqualTo = "Not Equal To",
|
||||
GreaterThan = "Greater Than",
|
||||
LessThan = "Less Than",
|
||||
GreaterThanOrEqualTo = "Greater Than Or Equal To",
|
||||
LessThanOrEqualTo = "Less Than Or Equal To",
|
||||
ContainsAny = "Contains Any",
|
||||
NotContains = "Not Contains",
|
||||
StartsWith = "Starts With",
|
||||
EndsWith = "Ends With",
|
||||
IsEmpty = "Is Empty",
|
||||
IsNotEmpty = "Is Not Empty",
|
||||
True = "True",
|
||||
False = "False",
|
||||
ContainsAll = "Contains All",
|
||||
}
|
||||
|
||||
export default interface NotificationRuleCondition {
|
||||
checkOn: NotificationRuleConditionCheckOn;
|
||||
conditionType: ConditionType | undefined;
|
||||
value: string | Array<string> | undefined;
|
||||
}
|
||||
|
||||
export class NotificationRuleConditionUtil {
|
||||
public static getValidationError(data: {
|
||||
notificationRule: IncidentNotificationRule;
|
||||
eventType: NotificationRuleEventType;
|
||||
workspaceType: WorkspaceType;
|
||||
}): string | null {
|
||||
const { notificationRule, eventType, workspaceType } = data;
|
||||
|
||||
for (const condition of notificationRule.filters) {
|
||||
if (!condition.checkOn) {
|
||||
return "Check On is required";
|
||||
}
|
||||
|
||||
if (!condition.conditionType) {
|
||||
return `Filter Condition is required for ${condition.checkOn}`;
|
||||
}
|
||||
|
||||
if (!condition.value) {
|
||||
return `Value is required for ${condition.checkOn}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(condition.value) && condition.value.length === 0) {
|
||||
return `Value is required for ${condition.checkOn}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
eventType === NotificationRuleEventType.Incident ||
|
||||
eventType === NotificationRuleEventType.Alert ||
|
||||
eventType === NotificationRuleEventType.ScheduledMaintenance
|
||||
) {
|
||||
// either create slack channel or select existing one should be active.
|
||||
|
||||
if (
|
||||
!notificationRule.shouldCreateNewChannel &&
|
||||
!notificationRule.shouldPostToExistingChannel
|
||||
) {
|
||||
return (
|
||||
"Please select either create slack channel or post to existing " +
|
||||
workspaceType +
|
||||
" channel"
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationRule.shouldPostToExistingChannel) {
|
||||
if (!notificationRule.existingChannelNames?.trim()) {
|
||||
return "Existing " + workspaceType + " channel name is required";
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationRule.shouldCreateNewChannel) {
|
||||
if (!notificationRule.newChannelTemplateName?.trim()) {
|
||||
return "New " + workspaceType + " channel name is required";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static hasValueField(data: {
|
||||
checkOn: NotificationRuleConditionCheckOn;
|
||||
conditionType: ConditionType | undefined;
|
||||
}): boolean {
|
||||
switch (data.conditionType) {
|
||||
case ConditionType.IsEmpty:
|
||||
case ConditionType.IsNotEmpty:
|
||||
case ConditionType.True:
|
||||
case ConditionType.False:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static isDropdownValueField(data: {
|
||||
checkOn: NotificationRuleConditionCheckOn | undefined;
|
||||
conditionType: ConditionType | undefined;
|
||||
}): boolean {
|
||||
// incident state, alert state, monitor status, scheduled maintenance state, severit, labels, monitors are all dropdowns.
|
||||
|
||||
if (!data.checkOn || !data.conditionType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (data.checkOn) {
|
||||
case NotificationRuleConditionCheckOn.MonitorType:
|
||||
case NotificationRuleConditionCheckOn.IncidentState:
|
||||
case NotificationRuleConditionCheckOn.AlertState:
|
||||
case NotificationRuleConditionCheckOn.MonitorStatus:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceState:
|
||||
case NotificationRuleConditionCheckOn.IncidentSeverity:
|
||||
case NotificationRuleConditionCheckOn.AlertSeverity:
|
||||
case NotificationRuleConditionCheckOn.MonitorLabels:
|
||||
case NotificationRuleConditionCheckOn.IncidentLabels:
|
||||
case NotificationRuleConditionCheckOn.AlertLabels:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels:
|
||||
case NotificationRuleConditionCheckOn.Monitors:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static getDropdownOptionsByCheckOn(data: {
|
||||
alertSeverities: Array<AlertSeverity>;
|
||||
alertStates: Array<AlertState>;
|
||||
incidentSeverities: Array<IncidentSeverity>;
|
||||
monitorStatus: Array<MonitorStatus>;
|
||||
incidentStates: Array<IncidentState>;
|
||||
scheduledMaintenanceStates: Array<ScheduledMaintenanceState>;
|
||||
labels: Array<Label>;
|
||||
monitors: Array<Monitor>;
|
||||
checkOn: NotificationRuleConditionCheckOn;
|
||||
}): Array<DropdownOption> {
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.AlertSeverity) {
|
||||
return data.alertSeverities.map((severity: AlertSeverity) => {
|
||||
return {
|
||||
value: severity.id!.toString(),
|
||||
label: severity.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.IncidentSeverity) {
|
||||
return data.incidentSeverities.map((severity: IncidentSeverity) => {
|
||||
return {
|
||||
value: severity.id!.toString(),
|
||||
label: severity.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.MonitorStatus) {
|
||||
return data.monitorStatus.map((status: MonitorStatus) => {
|
||||
return {
|
||||
value: status.id!.toString(),
|
||||
label: status.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.IncidentState) {
|
||||
return data.incidentStates.map((state: IncidentState) => {
|
||||
return {
|
||||
value: state.id!.toString(),
|
||||
label: state.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
data.checkOn ===
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceState
|
||||
) {
|
||||
return data.scheduledMaintenanceStates.map(
|
||||
(state: ScheduledMaintenanceState) => {
|
||||
return {
|
||||
value: state.id!.toString(),
|
||||
label: state.name || "",
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
data.checkOn === NotificationRuleConditionCheckOn.MonitorLabels ||
|
||||
data.checkOn === NotificationRuleConditionCheckOn.IncidentLabels ||
|
||||
data.checkOn === NotificationRuleConditionCheckOn.AlertLabels ||
|
||||
data.checkOn ===
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels
|
||||
) {
|
||||
return data.labels.map((label: Label) => {
|
||||
return {
|
||||
value: label.id!.toString(),
|
||||
label: label.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.Monitors) {
|
||||
return data.monitors.map((monitor: Monitor) => {
|
||||
return {
|
||||
value: monitor.id!.toString(),
|
||||
label: monitor.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// alert states
|
||||
if (data.checkOn === NotificationRuleConditionCheckOn.AlertState) {
|
||||
return data.alertStates.map((state: AlertState) => {
|
||||
return {
|
||||
value: state.id!.toString(),
|
||||
label: state.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static getCheckOnByEventType(
|
||||
eventType: NotificationRuleEventType,
|
||||
): Array<NotificationRuleConditionCheckOn> {
|
||||
switch (eventType) {
|
||||
case NotificationRuleEventType.Incident:
|
||||
return [
|
||||
NotificationRuleConditionCheckOn.IncidentTitle,
|
||||
NotificationRuleConditionCheckOn.IncidentDescription,
|
||||
NotificationRuleConditionCheckOn.IncidentSeverity,
|
||||
NotificationRuleConditionCheckOn.IncidentState,
|
||||
NotificationRuleConditionCheckOn.IncidentLabels,
|
||||
NotificationRuleConditionCheckOn.MonitorLabels,
|
||||
NotificationRuleConditionCheckOn.Monitors,
|
||||
];
|
||||
case NotificationRuleEventType.Alert:
|
||||
return [
|
||||
NotificationRuleConditionCheckOn.AlertTitle,
|
||||
NotificationRuleConditionCheckOn.AlertDescription,
|
||||
NotificationRuleConditionCheckOn.AlertSeverity,
|
||||
NotificationRuleConditionCheckOn.AlertState,
|
||||
|
||||
NotificationRuleConditionCheckOn.AlertLabels,
|
||||
NotificationRuleConditionCheckOn.MonitorLabels,
|
||||
|
||||
NotificationRuleConditionCheckOn.Monitors,
|
||||
];
|
||||
case NotificationRuleEventType.MonitorStatus:
|
||||
return [
|
||||
NotificationRuleConditionCheckOn.MonitorName,
|
||||
NotificationRuleConditionCheckOn.MonitorStatus,
|
||||
NotificationRuleConditionCheckOn.MonitorType,
|
||||
NotificationRuleConditionCheckOn.Monitors,
|
||||
NotificationRuleConditionCheckOn.MonitorLabels,
|
||||
];
|
||||
case NotificationRuleEventType.ScheduledMaintenance:
|
||||
return [
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle,
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription,
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceState,
|
||||
NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels,
|
||||
NotificationRuleConditionCheckOn.MonitorLabels,
|
||||
NotificationRuleConditionCheckOn.Monitors,
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static getConditionTypeByCheckOn(
|
||||
checkOn: NotificationRuleConditionCheckOn,
|
||||
): Array<ConditionType> {
|
||||
switch (checkOn) {
|
||||
case NotificationRuleConditionCheckOn.MonitorName:
|
||||
case NotificationRuleConditionCheckOn.IncidentTitle:
|
||||
case NotificationRuleConditionCheckOn.IncidentDescription:
|
||||
case NotificationRuleConditionCheckOn.AlertTitle:
|
||||
case NotificationRuleConditionCheckOn.AlertDescription:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription:
|
||||
return [
|
||||
ConditionType.EqualTo,
|
||||
ConditionType.NotEqualTo,
|
||||
ConditionType.ContainsAny,
|
||||
ConditionType.NotContains,
|
||||
ConditionType.StartsWith,
|
||||
ConditionType.EndsWith,
|
||||
];
|
||||
case NotificationRuleConditionCheckOn.IncidentSeverity:
|
||||
case NotificationRuleConditionCheckOn.AlertSeverity:
|
||||
return [ConditionType.ContainsAny, ConditionType.NotContains];
|
||||
case NotificationRuleConditionCheckOn.IncidentState:
|
||||
case NotificationRuleConditionCheckOn.AlertState:
|
||||
case NotificationRuleConditionCheckOn.MonitorStatus:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceState:
|
||||
return [ConditionType.ContainsAny, ConditionType.NotContains];
|
||||
case NotificationRuleConditionCheckOn.MonitorType:
|
||||
return [ConditionType.ContainsAny, ConditionType.NotContains];
|
||||
case NotificationRuleConditionCheckOn.AlertLabels:
|
||||
case NotificationRuleConditionCheckOn.IncidentLabels:
|
||||
case NotificationRuleConditionCheckOn.MonitorLabels:
|
||||
case NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels:
|
||||
return [
|
||||
ConditionType.ContainsAny,
|
||||
ConditionType.NotContains,
|
||||
ConditionType.ContainsAll,
|
||||
];
|
||||
case NotificationRuleConditionCheckOn.Monitors:
|
||||
return [
|
||||
ConditionType.ContainsAny,
|
||||
ConditionType.NotContains,
|
||||
ConditionType.ContainsAll,
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
|
||||
|
||||
export default interface AlertNotificationRule
|
||||
extends CreateChannelNotificationRule {
|
||||
_type: "AlertNotificationRule";
|
||||
|
||||
shouldAutomaticallyInviteOnCallUsersToNewChannel: boolean;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
|
||||
|
||||
export default interface IncidentNotificationRule
|
||||
extends CreateChannelNotificationRule {
|
||||
_type: "IncidentNotificationRule";
|
||||
|
||||
shouldAutomaticallyInviteOnCallUsersToNewChannel: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import BaseNotificationRule from "../BaseNotificationRule";
|
||||
|
||||
// This rule is just used to post to existing channels.
|
||||
export default interface MonitorStatusNotificationRule
|
||||
extends BaseNotificationRule {
|
||||
_type: "MonitorStatusNotificationRule";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
|
||||
|
||||
export default interface ScheduledMaintenanceNotificationRule
|
||||
extends CreateChannelNotificationRule {
|
||||
_type: "ScheduledMaintenanceNotificationRule";
|
||||
}
|
||||
261
Common/Types/Workspace/NotificationRules/NotificationRuleUtil.ts
Normal file
261
Common/Types/Workspace/NotificationRules/NotificationRuleUtil.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import FilterCondition from "../../Filter/FilterCondition";
|
||||
import {
|
||||
ConditionType,
|
||||
NotificationRuleConditionCheckOn,
|
||||
} from "./NotificationRuleCondition";
|
||||
import IncidentNotificationRule from "./NotificationRuleTypes/IncidentNotificationRule";
|
||||
|
||||
export class WorkspaceNotificationRuleUtil {
|
||||
public static isRuleMatching(data: {
|
||||
notificationRule: IncidentNotificationRule;
|
||||
values: {
|
||||
[key in NotificationRuleConditionCheckOn]:
|
||||
| string
|
||||
| Array<string>
|
||||
| undefined;
|
||||
};
|
||||
}): boolean {
|
||||
const notificationRule: IncidentNotificationRule = data.notificationRule;
|
||||
|
||||
// no filters means all filters are matched
|
||||
if (data.notificationRule.filters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterCondition: FilterCondition = notificationRule.filterCondition;
|
||||
|
||||
for (const filter of notificationRule.filters) {
|
||||
const value: string | Array<string> | undefined =
|
||||
data.values[filter.checkOn];
|
||||
const condition: ConditionType | undefined = filter.conditionType;
|
||||
const filterValue: string | Array<string> | undefined = filter.value;
|
||||
|
||||
if (!condition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isMatched: boolean = this.didConditionMatch({
|
||||
value,
|
||||
condition,
|
||||
filterValue,
|
||||
});
|
||||
|
||||
if (filterCondition === FilterCondition.All) {
|
||||
if (!isMatched) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterCondition === FilterCondition.Any) {
|
||||
if (isMatched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterCondition === FilterCondition.All) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filterCondition === FilterCondition.Any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static didConditionMatch(data: {
|
||||
value: string | Array<string> | undefined;
|
||||
condition: ConditionType;
|
||||
filterValue: string | Array<string> | undefined;
|
||||
}): boolean {
|
||||
const value: string | Array<string> | undefined = data.value;
|
||||
const condition: ConditionType = data.condition;
|
||||
const filterValue: string | Array<string> | undefined = data.filterValue;
|
||||
|
||||
if (value === undefined || filterValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (condition) {
|
||||
case ConditionType.EqualTo: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.includes(val);
|
||||
});
|
||||
}
|
||||
return value === filterValue;
|
||||
}
|
||||
|
||||
case ConditionType.NotEqualTo: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return !value.every((val: string) => {
|
||||
return filterValue.includes(val);
|
||||
});
|
||||
}
|
||||
return value !== filterValue;
|
||||
}
|
||||
|
||||
case ConditionType.GreaterThan: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val > fVal;
|
||||
});
|
||||
});
|
||||
} else if (value !== undefined && filterValue !== undefined) {
|
||||
return value > filterValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.LessThan: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val < fVal;
|
||||
});
|
||||
});
|
||||
} else if (value !== undefined && filterValue !== undefined) {
|
||||
return value < filterValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.GreaterThanOrEqualTo: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val >= fVal;
|
||||
});
|
||||
});
|
||||
} else if (value !== undefined && filterValue !== undefined) {
|
||||
return value >= filterValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.LessThanOrEqualTo: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val <= fVal;
|
||||
});
|
||||
});
|
||||
} else if (value !== undefined && filterValue !== undefined) {
|
||||
return value <= filterValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.ContainsAny: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.some((val: string) => {
|
||||
return filterValue.includes(val);
|
||||
});
|
||||
} else if (
|
||||
value !== undefined &&
|
||||
filterValue !== undefined &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
return filterValue.includes(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.NotContains: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return !value.some((val: string) => {
|
||||
return filterValue.includes(val);
|
||||
});
|
||||
} else if (
|
||||
value !== undefined &&
|
||||
filterValue !== undefined &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
return !filterValue.includes(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.StartsWith: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val.startsWith(fVal);
|
||||
});
|
||||
});
|
||||
} else if (
|
||||
value !== undefined &&
|
||||
filterValue !== undefined &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
return value.startsWith(filterValue.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.EndsWith: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return value.every((val: string) => {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return val.endsWith(fVal);
|
||||
});
|
||||
});
|
||||
} else if (
|
||||
value !== undefined &&
|
||||
filterValue !== undefined &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
return value.endsWith(filterValue.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case ConditionType.IsEmpty: {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
return value === "" || value === undefined;
|
||||
}
|
||||
|
||||
case ConditionType.IsNotEmpty: {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
return value !== "" && value !== undefined;
|
||||
}
|
||||
|
||||
case ConditionType.True: {
|
||||
if (Array.isArray(value)) {
|
||||
return value.every((val: string) => {
|
||||
return val === "true";
|
||||
});
|
||||
}
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
case ConditionType.False: {
|
||||
if (Array.isArray(value)) {
|
||||
return value.every((val: string) => {
|
||||
return val === "false";
|
||||
});
|
||||
}
|
||||
return value === "false";
|
||||
}
|
||||
|
||||
case ConditionType.ContainsAll: {
|
||||
if (Array.isArray(value) && Array.isArray(filterValue)) {
|
||||
return filterValue.every((fVal: string) => {
|
||||
return value.includes(fVal);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default interface WorkspaceChannelInvitationPayload {
|
||||
workspaceUserIds: Array<string>;
|
||||
channelNames: Array<string>;
|
||||
}
|
||||
30
Common/Types/Workspace/WorkspaceMessagePayload.ts
Normal file
30
Common/Types/Workspace/WorkspaceMessagePayload.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface WorkspaceMessageBlock {
|
||||
_type: string;
|
||||
onlyPostToTheseChannelNames?: Array<string>;
|
||||
}
|
||||
|
||||
export interface WorkspaceMessagePayloadButton {
|
||||
title: string; // Button title.
|
||||
onlyPostToTheseChannelNames?: Array<string>; // Channel names to send message to.
|
||||
}
|
||||
|
||||
export interface WorkspacePayloadHeader extends WorkspaceMessageBlock {
|
||||
_type: "WorkspacePayloadHeader";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WorkspacePayloadMarkdown extends WorkspaceMessageBlock {
|
||||
_type: "WorkspacePayloadMarkdown";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WorkspacePayloadButtons extends WorkspaceMessageBlock {
|
||||
_type: "WorkspacePayloadButtons";
|
||||
buttons: Array<WorkspaceMessagePayloadButton>;
|
||||
}
|
||||
|
||||
export default interface WorkspaceMessagePayload {
|
||||
_type: "WorkspaceMessagePayload";
|
||||
channelNames: Array<string>; // Channel ids to send message to.
|
||||
messageBlocks: Array<WorkspaceMessageBlock>; // Message to add to blocks.
|
||||
}
|
||||
6
Common/Types/Workspace/WorkspaceType.ts
Normal file
6
Common/Types/Workspace/WorkspaceType.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
enum WorkspaceType {
|
||||
Slack = "Slack",
|
||||
MicrosoftTeams = "MicrosoftTeams",
|
||||
}
|
||||
|
||||
export default WorkspaceType;
|
||||
@@ -32,7 +32,7 @@ const Card: FunctionComponent<ComponentProps> = (
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={props.className}>
|
||||
<div data-testid="card" className={props.className}>
|
||||
<div className="shadow sm:rounded-md">
|
||||
<div className="bg-white py-6 px-4 sm:p-6">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -3,9 +3,11 @@ import UiAnalytics from "../../Utils/Analytics";
|
||||
import Alert, { AlertType } from "../Alerts/Alert";
|
||||
import Button, { ButtonStyleType } from "../Button/Button";
|
||||
import ButtonTypes from "../Button/ButtonTypes";
|
||||
|
||||
import { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
|
||||
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
||||
import FormField from "./Fields/FormField";
|
||||
import FormSummary from "./FormSummary";
|
||||
import Steps from "./Steps/Steps";
|
||||
import Field from "./Types/Field";
|
||||
import Fields from "./Types/Fields";
|
||||
@@ -48,6 +50,11 @@ export const DefaultValidateFunction: DefaultValidateFunctionType = (
|
||||
return {};
|
||||
};
|
||||
|
||||
export interface FormSummaryConfig {
|
||||
enabled: boolean;
|
||||
defaultStepName?: string | undefined;
|
||||
}
|
||||
|
||||
export interface BaseComponentProps<T> {
|
||||
submitButtonStyleType?: ButtonStyleType | undefined;
|
||||
initialValues?: FormValues<T> | undefined;
|
||||
@@ -73,6 +80,7 @@ export interface BaseComponentProps<T> {
|
||||
onIsLastFormStep?: undefined | ((isLastFormStep: boolean) => void);
|
||||
onFormValidationErrorChanged?: ((hasError: boolean) => void) | undefined;
|
||||
showSubmitButtonOnlyIfSomethingChanged?: boolean | undefined;
|
||||
summary?: FormSummaryConfig | undefined;
|
||||
}
|
||||
|
||||
export interface ComponentProps<T extends GenericObject>
|
||||
@@ -104,12 +112,33 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
setIsLoading(props.isLoading);
|
||||
}, [props.isLoading]);
|
||||
|
||||
const getFormSteps: () => Array<FormStep<T>> | undefined = () => {
|
||||
if (props.summary && props.summary.enabled) {
|
||||
// add to last step
|
||||
return [
|
||||
...(props.steps || [
|
||||
{
|
||||
id: props.summary.defaultStepName || "basic",
|
||||
title: props.summary.defaultStepName || "Basic",
|
||||
isSummaryStep: false,
|
||||
},
|
||||
]),
|
||||
{
|
||||
id: "summary",
|
||||
title: "Summary",
|
||||
isSummaryStep: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return props.steps;
|
||||
};
|
||||
|
||||
const [submitButtonText, setSubmitButtonText] = useState<string>(
|
||||
props.submitButtonText || "Submit",
|
||||
);
|
||||
|
||||
const [formSteps, setFormSteps] = useState<Array<FormStep<T>> | undefined>(
|
||||
props.steps,
|
||||
getFormSteps(),
|
||||
);
|
||||
|
||||
const isInitialValuesSet: MutableRefObject<boolean> = useRef(false);
|
||||
@@ -175,7 +204,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
setFormSteps(
|
||||
props.steps?.filter((step: FormStep<T>) => {
|
||||
getFormSteps()?.filter((step: FormStep<T>) => {
|
||||
if (!step.showIf) {
|
||||
return true;
|
||||
}
|
||||
@@ -240,11 +269,26 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
|
||||
for (const item of fields) {
|
||||
// if this field is not the current step.
|
||||
|
||||
let shouldSkip: boolean = false;
|
||||
if (
|
||||
currentFormStepId &&
|
||||
item.stepId &&
|
||||
item.stepId !== currentFormStepId
|
||||
) {
|
||||
shouldSkip = true;
|
||||
}
|
||||
|
||||
if (
|
||||
props.summary?.enabled &&
|
||||
(!props.steps || props.steps.length === 0)
|
||||
) {
|
||||
// if summary is enabled and no steps are provided, then all fields belong to the same step and should not be skipped.
|
||||
shouldSkip = false;
|
||||
item.stepId = props.summary.defaultStepName || "basic";
|
||||
}
|
||||
|
||||
if (shouldSkip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -536,7 +580,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
|
||||
<div className="flex">
|
||||
{formSteps && currentFormStepId && (
|
||||
<div className="w-1/3">
|
||||
<div style={{ flex: "0 1 auto" }} className="mr-10">
|
||||
{/* Form Steps */}
|
||||
|
||||
<Steps
|
||||
@@ -551,8 +595,9 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
formSteps && currentFormStepId ? "w-2/3 pt-6" : "w-full pt-1"
|
||||
formSteps && currentFormStepId ? "w-auto pt-6" : "w-full pt-1"
|
||||
}`}
|
||||
style={{ flex: "1 1 auto" }}
|
||||
>
|
||||
{props.error && (
|
||||
<div className="mb-3">
|
||||
@@ -575,6 +620,15 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((field: Field<T>) => {
|
||||
const currentValues: FormValues<T> =
|
||||
refCurrentValue.current;
|
||||
if (field.showIf && !field.showIf(currentValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((field: Field<T>, i: number) => {
|
||||
return (
|
||||
<div key={getFieldName(field)}>
|
||||
@@ -605,6 +659,16 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* If Summary, show Model detail */}
|
||||
|
||||
{currentFormStepId === "summary" && (
|
||||
<FormSummary
|
||||
formValues={refCurrentValue.current}
|
||||
formFields={formFields}
|
||||
formSteps={formSteps || undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user